Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/components/dashboard/AdvancedSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -803,10 +803,10 @@

{searchAnalytics && (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' }}>
<AnalyticsChip label="Searches" value={searchAnalytics.totalSearches} />

Check failure on line 806 in src/components/dashboard/AdvancedSearch.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format Check

'AnalyticsChip' is not defined
<AnalyticsChip label="Cache hits" value={searchAnalytics.cachedSearches} />

Check failure on line 807 in src/components/dashboard/AdvancedSearch.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format Check

'AnalyticsChip' is not defined
<AnalyticsChip label="Saved" value={searchAnalytics.savedSearches} />

Check failure on line 808 in src/components/dashboard/AdvancedSearch.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format Check

'AnalyticsChip' is not defined
<AnalyticsChip label="Alerts" value={searchAnalytics.alerts} />

Check failure on line 809 in src/components/dashboard/AdvancedSearch.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format Check

'AnalyticsChip' is not defined
</div>
)}

Expand Down Expand Up @@ -919,7 +919,7 @@
</div>
</div>
)}
{/* end keyword search bar */}


{/* Search Results */}
{!semanticMode && searchResults && (
Expand Down
75 changes: 73 additions & 2 deletions src/components/dashboard/CustomReports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ import {
createReportSchedule,
exportReportAsCsv,
exportReportAsJson,
getAllReportTemplates,
getNextRunPreview,
getReportTemplate,
loadSavedReportTemplates,
parseNaturalLanguageRequest,
REPORT_COMPONENT_PALETTE,
REPORT_TEMPLATES,
saveReportTemplate,
transformReportData,
type ReportComponentDefinition,
type ReportResource,
Expand Down Expand Up @@ -80,9 +84,15 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
const [email, setEmail] = useState("");
const [webhookUrl, setWebhookUrl] = useState("");
const [components, setComponents] = useState(REPORT_TEMPLATES[0].components);
const [requestText, setRequestText] = useState("Create a weekly account activity report for GABC with emphasis on risks and fees");
const [savedTemplates, setSavedTemplates] = useState(loadSavedReportTemplates());

const template = getReportTemplate(templateId);
const reportData = useMemo(() => transformReportData(template.id, analytics || {}), [analytics, template.id]);
const parsedRequest = useMemo(() => parseNaturalLanguageRequest(requestText), [requestText]);
const reportData = useMemo(
() => transformReportData(template.id, analytics || {}, { focusAreas: parsedRequest.focusAreas, request: parsedRequest.prompt }),
[analytics, parsedRequest.focusAreas, parsedRequest.prompt, template.id],
);
const query = useMemo(
() =>
buildHorizonQuery({
Expand Down Expand Up @@ -118,6 +128,27 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
setComponents(nextTemplate.components);
};

const handleRequestApply = () => {
const nextTemplate = getReportTemplate(parsedRequest.templateId);
setTemplateId(nextTemplate.id);
setResource(nextTemplate.resource);
setAccountId(parsedRequest.accountId || accountId);
setComponents(nextTemplate.components);
};

const handleSaveTemplate = () => {
const saved = saveReportTemplate({
...template,
id: `${template.id}-saved-${Date.now()}`,
name: `${template.name} (Saved)`,
description: `${template.description} Saved from the dashboard.`,
category: template.category || "custom",
tags: [...(template.tags || []), "saved"],
});
setSavedTemplates(loadSavedReportTemplates());
return saved;
};

const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const droppedId = event.dataTransfer.getData("application/report-component");
Expand Down Expand Up @@ -164,6 +195,9 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
</div>
</div>
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
<button type="button" style={buttonStyle} onClick={handleSaveTemplate} title="Save current template">
<Save size={14} /> Save
</button>
<button type="button" style={buttonStyle} onClick={exportCsv} title="Export CSV">
<Download size={14} /> CSV
</button>
Expand All @@ -181,14 +215,23 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
<label style={{ display: "flex", flexDirection: "column", gap: "6px", fontSize: "12px", color: "var(--text-muted)" }}>
Template
<select value={templateId} onChange={(event) => handleTemplateChange(event.target.value)} style={fieldStyle}>
{REPORT_TEMPLATES.map((reportTemplate) => (
{getAllReportTemplates().map((reportTemplate) => (
<option key={reportTemplate.id} value={reportTemplate.id}>
{reportTemplate.name}
</option>
))}
</select>
</label>

<label style={{ display: "flex", flexDirection: "column", gap: "6px", fontSize: "12px", color: "var(--text-muted)" }}>
Prompt
<textarea value={requestText} onChange={(event) => setRequestText(event.target.value)} rows={3} style={{ ...fieldStyle, minHeight: "80px" }} />
</label>

<button type="button" style={buttonStyle} onClick={handleRequestApply}>
Generate report from prompt
</button>

<div style={{ display: "grid", gridTemplateColumns: "1fr 90px", gap: "8px" }}>
<label style={{ display: "flex", flexDirection: "column", gap: "6px", fontSize: "12px", color: "var(--text-muted)" }}>
Horizon resource
Expand Down Expand Up @@ -299,6 +342,34 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
))}
</div>

{reportData.insights && reportData.insights.length > 0 && (
<div style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", borderRadius: "var(--radius-md)", padding: "10px" }}>
<div style={{ fontSize: "12px", color: "var(--text-muted)", marginBottom: "6px" }}>AI insights</div>
<ul style={{ margin: 0, paddingLeft: "16px", display: "grid", gap: "6px", fontSize: "12px" }}>
{reportData.insights.map((insight) => (
<li key={insight}>{insight}</li>
))}
</ul>
</div>
)}

<div style={{ fontSize: "12px", color: "var(--text-muted)" }}>
Parsed focus: {parsedRequest.focusAreas.join(", ")} {parsedRequest.accountId ? `· account ${parsedRequest.accountId}` : ""}
</div>

{savedTemplates.length > 0 && (
<div style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", borderRadius: "var(--radius-md)", padding: "10px" }}>
<div style={{ fontSize: "12px", color: "var(--text-muted)", marginBottom: "6px" }}>Saved templates</div>
<div style={{ display: "grid", gap: "6px" }}>
{savedTemplates.map((item) => (
<button key={item.id} type="button" style={{ ...buttonStyle, justifyContent: "flex-start" }} onClick={() => handleTemplateChange(item.id)}>
{item.name}
</button>
))}
</div>
</div>
)}

<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }}>
<label style={{ display: "flex", flexDirection: "column", gap: "6px", fontSize: "12px", color: "var(--text-muted)" }}>
Cron
Expand Down
33 changes: 27 additions & 6 deletions src/lib/__tests__/customReports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
exportReportAsCsv,
exportReportAsJson,
getNextRunPreview,
loadSavedReportTemplates,
parseNaturalLanguageRequest,
REPORT_TEMPLATES,
saveReportTemplate,
transformReportData,
} from "../customReports";

Expand Down Expand Up @@ -38,15 +41,33 @@ const analytics = {

describe("custom reports", () => {
it("ships the required Stellar report templates", () => {
expect(REPORT_TEMPLATES.map((template) => template.id)).toEqual([
"account-activity",
"portfolio-performance",
"transaction-analysis",
"network-health",
]);
expect(REPORT_TEMPLATES.length).toBeGreaterThanOrEqual(20);
expect(REPORT_TEMPLATES.map((template) => template.id)).toContain("account-activity");
expect(REPORT_TEMPLATES.map((template) => template.id)).toContain("network-health");
expect(REPORT_TEMPLATES.every((template) => template.components.length >= 3)).toBe(true);
});

it("parses natural-language requests and persists saved report templates", () => {
const parsed = parseNaturalLanguageRequest("Create a weekly account activity report for GABC with emphasis on risks and fees");

expect(parsed.templateId).toBe("account-activity");
expect(parsed.accountId).toBe("GABC");
expect(parsed.focusAreas).toEqual(expect.arrayContaining(["risks", "fees"]));

const saved = saveReportTemplate({
id: "saved-weekly-account",
name: "Weekly Account Summary",
description: "A saved report template",
resource: "accounts",
components: REPORT_TEMPLATES[0].components,
category: "account",
tags: ["weekly", "account"],
});

expect(saved.id).toBe("saved-weekly-account");
expect(loadSavedReportTemplates()).toEqual(expect.arrayContaining([expect.objectContaining({ id: "saved-weekly-account" })]));
});

it("builds account-scoped Horizon queries with filters", () => {
const query = buildHorizonQuery({
resource: "transactions",
Expand Down
Loading
Loading