@agent-native/core 0.98.2 → 0.98.4
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +23 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +2 -2
- package/corpus/core/src/client/analytics.ts +12 -0
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/session-replay.ts +612 -26
- package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
- package/corpus/core/src/client/settings/SettingsPanel.tsx +31 -211
- package/corpus/core/src/client/settings/agent-settings-search.ts +219 -0
- package/corpus/core/src/client/settings/index.ts +4 -0
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
- package/corpus/core/src/demo/config.ts +6 -6
- package/corpus/core/src/demo/fetch-interceptor.ts +25 -2
- package/corpus/core/src/demo/redact.ts +57 -66
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +65 -6
- package/corpus/templates/analytics/AGENTS.md +6 -6
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +429 -244
- package/corpus/templates/analytics/app/components/layout/command-palette-search.ts +93 -0
- package/corpus/templates/analytics/app/global.css +4 -62
- package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +2 -1
- package/corpus/templates/analytics/app/lib/demo-chart-trend.ts +46 -33
- package/corpus/templates/analytics/app/pages/Settings.tsx +8 -45
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +413 -105
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +32 -16
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +126 -0
- package/corpus/templates/analytics/changelog/2026-07-12-command-menu-loading-placeholders-now-stay-below-available-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-command-menu-search-now-prioritizes-the-best-match.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-demo-charts-preserve-source-movement.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-fixed-session-replays-that-appeared-ultra-wide-hid-recorded-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-replay-storage-settings-stay-readable-at-narrow-widths.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-replays-load-faster-in-demo-mode-while-visitor-email.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-replays-preserve-recorded-styling-and-keep-malformed.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +11 -2
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +14 -2
- package/corpus/templates/calendar/changelog/2026-07-12-calendar-responses-can-now-be-saved-with-command-enter-or-ct.md +6 -0
- package/dist/agent/production-agent.js +2 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +11 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +12 -3
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +488 -23
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +3 -3
- package/dist/client/settings/DemoModeSection.js +4 -4
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +41 -191
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/agent-settings-search.d.ts +16 -0
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -0
- package/dist/client/settings/agent-settings-search.js +178 -0
- package/dist/client/settings/agent-settings-search.js.map +1 -0
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -0
- package/dist/client/settings/index.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +2 -2
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/config.js +6 -6
- package/dist/demo/config.js.map +1 -1
- package/dist/demo/fetch-interceptor.d.ts +1 -0
- package/dist/demo/fetch-interceptor.d.ts.map +1 -1
- package/dist/demo/fetch-interceptor.js +19 -2
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/demo/redact.d.ts +14 -5
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +30 -56
- package/dist/demo/redact.js.map +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/package.json +2 -2
|
@@ -26,3 +26,96 @@ export function commandPaletteKeywords(
|
|
|
26
26
|
|
|
27
27
|
return Array.from(variants);
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
function normalizeSearchText(value: string): string {
|
|
31
|
+
return value
|
|
32
|
+
.normalize("NFKD")
|
|
33
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fuzzySubsequenceScore(candidate: string, query: string): number {
|
|
40
|
+
const compactCandidate = candidate.replace(/\s/g, "");
|
|
41
|
+
const compactQuery = query.replace(/\s/g, "");
|
|
42
|
+
let queryIndex = 0;
|
|
43
|
+
|
|
44
|
+
for (const character of compactCandidate) {
|
|
45
|
+
if (character === compactQuery[queryIndex]) queryIndex += 1;
|
|
46
|
+
if (queryIndex === compactQuery.length) {
|
|
47
|
+
return 0.2 * (compactQuery.length / compactCandidate.length);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function commandPaletteFilter(
|
|
55
|
+
value: string,
|
|
56
|
+
search: string,
|
|
57
|
+
keywords: string[] = [],
|
|
58
|
+
): number {
|
|
59
|
+
const query = normalizeSearchText(search);
|
|
60
|
+
if (!query) return 1;
|
|
61
|
+
|
|
62
|
+
const queryWords = query.split(" ");
|
|
63
|
+
let bestScore = 0;
|
|
64
|
+
|
|
65
|
+
for (const rawCandidate of [...keywords, value]) {
|
|
66
|
+
const candidate = normalizeSearchText(rawCandidate);
|
|
67
|
+
if (!candidate) continue;
|
|
68
|
+
|
|
69
|
+
const words = candidate.split(" ");
|
|
70
|
+
let score = 0;
|
|
71
|
+
|
|
72
|
+
if (candidate === query) score = 1;
|
|
73
|
+
else if (candidate.startsWith(query)) score = 0.96;
|
|
74
|
+
else if (words.includes(query)) score = 0.94;
|
|
75
|
+
else if (words.some((word) => word.startsWith(query))) score = 0.9;
|
|
76
|
+
else if (candidate.includes(query)) score = 0.8;
|
|
77
|
+
else if (
|
|
78
|
+
queryWords.every((queryWord) =>
|
|
79
|
+
words.some((word) => word.startsWith(queryWord)),
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
score = 0.72;
|
|
83
|
+
} else {
|
|
84
|
+
score = fuzzySubsequenceScore(candidate, query);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
bestScore = Math.max(bestScore, score);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return bestScore;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function rankCommandPaletteEntries<T>(
|
|
94
|
+
entries: T[],
|
|
95
|
+
search: string,
|
|
96
|
+
getSearchData: (entry: T) => { value: string; keywords?: string[] },
|
|
97
|
+
): Array<{ entry: T; score: number }> {
|
|
98
|
+
return entries
|
|
99
|
+
.map((entry, index) => {
|
|
100
|
+
const { value, keywords } = getSearchData(entry);
|
|
101
|
+
return {
|
|
102
|
+
entry,
|
|
103
|
+
index,
|
|
104
|
+
score: commandPaletteFilter(value, search, keywords),
|
|
105
|
+
};
|
|
106
|
+
})
|
|
107
|
+
.filter(({ score }) => score > 0)
|
|
108
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
109
|
+
.map(({ entry, score }) => ({ entry, score }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function uniqueCommandItems<T extends { id: string; name: string }>(
|
|
113
|
+
items: T[],
|
|
114
|
+
): T[] {
|
|
115
|
+
const seenIds = new Set<string>();
|
|
116
|
+
return items.filter((item) => {
|
|
117
|
+
if (seenIds.has(item.id)) return false;
|
|
118
|
+
seenIds.add(item.id);
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -245,72 +245,14 @@
|
|
|
245
245
|
transform-origin: top left;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
/*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
inside `.replayer-wrapper`, so it scales with the fit-to-stage transform. */
|
|
253
|
-
.an-replay-stage-root .replayer-mouse {
|
|
254
|
-
position: absolute;
|
|
255
|
-
z-index: 2;
|
|
256
|
-
width: 1.125rem;
|
|
257
|
-
height: 1.5rem;
|
|
258
|
-
border-radius: 0;
|
|
259
|
-
background: transparent;
|
|
260
|
-
background-image: none;
|
|
261
|
-
box-shadow: none;
|
|
262
|
-
transform: none;
|
|
263
|
-
transition:
|
|
264
|
-
left 80ms linear,
|
|
265
|
-
top 80ms linear;
|
|
266
|
-
pointer-events: none;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.an-replay-stage-root .replayer-mouse::before {
|
|
270
|
-
content: "";
|
|
271
|
-
position: absolute;
|
|
272
|
-
inset: 0;
|
|
273
|
-
background: hsl(0 0% 100%);
|
|
274
|
-
clip-path: polygon(0 0, 0 77%, 30% 57%, 48% 100%, 68% 92%, 50% 51%, 92% 51%);
|
|
275
|
-
filter: drop-shadow(0 0 1px hsl(0 0% 0% / 0.95))
|
|
276
|
-
drop-shadow(0 1px 3px hsl(0 0% 0% / 0.35));
|
|
277
|
-
}
|
|
278
|
-
|
|
248
|
+
/* IMPORTANT: Keep rrweb's stock SVG arrow. Only suppress its permanently
|
|
249
|
+
visible purple ::after disc, which can otherwise read as the cursor itself
|
|
250
|
+
when the arrow is small. The stock .active animation still supplies the
|
|
251
|
+
brief click halo. Do not replace the arrow with an app-colored dot. */
|
|
279
252
|
.an-replay-stage-root .replayer-mouse::after {
|
|
280
|
-
content: "";
|
|
281
|
-
position: absolute;
|
|
282
|
-
left: -0.5rem;
|
|
283
|
-
top: -0.5rem;
|
|
284
|
-
width: 1rem;
|
|
285
|
-
height: 1rem;
|
|
286
|
-
border-radius: 9999px;
|
|
287
|
-
border: 2px solid hsl(217 91% 60%);
|
|
288
|
-
background: transparent;
|
|
289
253
|
opacity: 0;
|
|
290
254
|
}
|
|
291
255
|
|
|
292
|
-
/* rrweb toggles `.active` on mouse interactions; pulse a click ripple. */
|
|
293
|
-
.an-replay-stage-root .replayer-mouse.active::after {
|
|
294
|
-
animation: an-replay-mouse-click 0.4s ease-out 1;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@keyframes an-replay-mouse-click {
|
|
298
|
-
0% {
|
|
299
|
-
opacity: 0.5;
|
|
300
|
-
transform: scale(1);
|
|
301
|
-
}
|
|
302
|
-
100% {
|
|
303
|
-
opacity: 0;
|
|
304
|
-
transform: scale(3.5);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
.an-replay-stage-root .replayer-mouse-tail {
|
|
309
|
-
position: absolute;
|
|
310
|
-
inset: 0;
|
|
311
|
-
pointer-events: none;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
256
|
.analytics-session-snippet .plan-code-surface {
|
|
315
257
|
border: 0;
|
|
316
258
|
border-radius: 0;
|
|
@@ -46,11 +46,12 @@ export async function fetchReplayStorageStatus(): Promise<ReplayStorageStatus> {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function useReplayStorageStatus() {
|
|
49
|
+
export function useReplayStorageStatus(options?: { enabled?: boolean }) {
|
|
50
50
|
return useQuery({
|
|
51
51
|
queryKey: REPLAY_STORAGE_STATUS_KEY,
|
|
52
52
|
queryFn: fetchReplayStorageStatus,
|
|
53
53
|
staleTime: 60_000,
|
|
54
|
+
enabled: options?.enabled,
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -52,49 +52,56 @@ function normalizedVolatility(
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function normalizedTrend(
|
|
55
|
-
|
|
55
|
+
series: number[],
|
|
56
|
+
minimum: number,
|
|
57
|
+
range: number,
|
|
56
58
|
volatilityScore: number,
|
|
57
59
|
random: () => number,
|
|
58
60
|
): number[] {
|
|
61
|
+
const length = series.length;
|
|
59
62
|
if (length <= 1) return [0];
|
|
60
63
|
if (length === 2) return [0, 1];
|
|
61
64
|
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const normalized = series.map((value) => (value - minimum) / range);
|
|
66
|
+
const randomization = 0.012 + volatilityScore * 0.08;
|
|
67
|
+
const shape = [normalized[0]];
|
|
68
|
+
|
|
69
|
+
// Preserve the source's actual step pattern (including when and how sharply
|
|
70
|
+
// it spikes), with just enough seeded variation that otherwise-similar demo
|
|
71
|
+
// series do not become identical. Smooth sources receive almost no jitter;
|
|
72
|
+
// volatile sources can vary a little more without moving their events.
|
|
73
|
+
for (let index = 1; index < length; index += 1) {
|
|
74
|
+
const sourceStep = normalized[index] - normalized[index - 1];
|
|
75
|
+
const factor = 1 + (random() * 2 - 1) * randomization;
|
|
76
|
+
shape.push(shape[index - 1] + sourceStep * factor);
|
|
77
|
+
}
|
|
68
78
|
|
|
79
|
+
// Add only the linear drift required for the first point to be the global
|
|
80
|
+
// minimum and the last to be the global maximum. A linear term has zero
|
|
81
|
+
// second difference, so the source's local acceleration, spikes, and dips
|
|
82
|
+
// survive instead of being replaced by a synthetic random walk.
|
|
83
|
+
let requiredDrift = 0;
|
|
84
|
+
for (let index = 1; index < length; index += 1) {
|
|
85
|
+
const progress = index / (length - 1);
|
|
86
|
+
requiredDrift = Math.max(
|
|
87
|
+
requiredDrift,
|
|
88
|
+
(shape[0] - shape[index]) / progress,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (let index = 0; index < length - 1; index += 1) {
|
|
69
92
|
const progress = index / (length - 1);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Pullback count and depth follow the source's normalized volatility. A
|
|
76
|
-
// smooth source remains a gentle rise; a jagged source gets several seeded
|
|
77
|
-
// reversals. Spacing the reversals across the series prevents them from
|
|
78
|
-
// collapsing into one noisy patch.
|
|
79
|
-
if (length >= 5 && volatilityScore >= 0.12) {
|
|
80
|
-
const maximumPullbacks = Math.min(3, Math.floor((length - 2) / 2));
|
|
81
|
-
const pullbackCount = Math.max(
|
|
82
|
-
1,
|
|
83
|
-
Math.round(volatilityScore * maximumPullbacks),
|
|
93
|
+
requiredDrift = Math.max(
|
|
94
|
+
requiredDrift,
|
|
95
|
+
(shape[index] - shape[length - 1]) / (1 - progress),
|
|
84
96
|
);
|
|
85
|
-
for (let pullback = 0; pullback < pullbackCount; pullback += 1) {
|
|
86
|
-
const segmentCenter =
|
|
87
|
-
((pullback + 1) * (length - 3)) / (pullbackCount + 1) + 1;
|
|
88
|
-
const jitter = (random() - 0.5) * Math.max(1, length / 10);
|
|
89
|
-
const dipIndex = Math.round(clamp(segmentCenter + jitter, 2, length - 2));
|
|
90
|
-
const center = dipIndex / (length - 1);
|
|
91
|
-
const amplitude = 0.025 + volatilityScore * (0.075 + random() * 0.12);
|
|
92
|
-
values[dipIndex - 1] = clamp(center + amplitude, 0.075, 0.925);
|
|
93
|
-
values[dipIndex] = clamp(center - amplitude, 0.05, 0.9);
|
|
94
|
-
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
const drift = requiredDrift + 0.012 + random() * 0.018;
|
|
100
|
+
const candidate = shape.map(
|
|
101
|
+
(value, index) => value + drift * (index / (length - 1)),
|
|
102
|
+
);
|
|
103
|
+
const candidateRange = candidate[length - 1] - candidate[0];
|
|
104
|
+
return candidate.map((value) => (value - candidate[0]) / candidateRange);
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/**
|
|
@@ -133,7 +140,13 @@ export function createDemoChartTrendRows(
|
|
|
133
140
|
minimum,
|
|
134
141
|
range,
|
|
135
142
|
);
|
|
136
|
-
const trend = normalizedTrend(
|
|
143
|
+
const trend = normalizedTrend(
|
|
144
|
+
points.map((point) => point.numeric),
|
|
145
|
+
minimum,
|
|
146
|
+
range,
|
|
147
|
+
volatilityScore,
|
|
148
|
+
random,
|
|
149
|
+
);
|
|
137
150
|
const useStringValues = points.every(
|
|
138
151
|
(point) => typeof point.original === "string",
|
|
139
152
|
);
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
useAgentSettingsTabs,
|
|
6
6
|
useSession,
|
|
7
7
|
useT,
|
|
8
|
-
type SettingsSearchEntry,
|
|
9
8
|
type SettingsTabItem,
|
|
10
9
|
} from "@agent-native/core/client";
|
|
11
10
|
import { TeamPage } from "@agent-native/core/client/org";
|
|
@@ -27,6 +26,7 @@ import changelog from "../../CHANGELOG.md?raw";
|
|
|
27
26
|
import { useReplayStorageStatus } from "../hooks/use-replay-storage-status";
|
|
28
27
|
import { ReplayStorageHint } from "./sessions/SessionsPage";
|
|
29
28
|
import { AlertRulesSettingsCard } from "./settings/AlertRulesSettingsCard";
|
|
29
|
+
import { buildAnalyticsGeneralSettingsSearchEntries } from "./settings/settings-search";
|
|
30
30
|
|
|
31
31
|
export default function Settings() {
|
|
32
32
|
// Settings is also reachable directly from the full-page agent surface.
|
|
@@ -56,49 +56,12 @@ export default function Settings() {
|
|
|
56
56
|
[agentSettingsTabs, t],
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
const generalSearchEntries = useMemo
|
|
60
|
-
() =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
hash: "account",
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: "analytics-credentials",
|
|
69
|
-
label: t("settings.credentials"),
|
|
70
|
-
keywords: "data sources api keys manage credentials",
|
|
71
|
-
hash: "credentials",
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: "analytics-dashboard-templates",
|
|
75
|
-
label: t("settings.dashboardTemplates"),
|
|
76
|
-
keywords: "templates catalog dashboards",
|
|
77
|
-
hash: "dashboard-templates",
|
|
78
|
-
},
|
|
79
|
-
...(replayStorageStatus.data?.configured
|
|
80
|
-
? [
|
|
81
|
-
{
|
|
82
|
-
id: "analytics-replay-storage",
|
|
83
|
-
label: t("sessions.storageSetupTitle"),
|
|
84
|
-
keywords: "session replay recording storage s3 bucket builder",
|
|
85
|
-
hash: "replay-storage",
|
|
86
|
-
},
|
|
87
|
-
]
|
|
88
|
-
: []),
|
|
89
|
-
{
|
|
90
|
-
id: "analytics-language",
|
|
91
|
-
label: t("settings.languageTitle"),
|
|
92
|
-
keywords: "language locale translation i18n",
|
|
93
|
-
hash: "language",
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "analytics-about",
|
|
97
|
-
label: t("settings.about"),
|
|
98
|
-
keywords: "about version info usage",
|
|
99
|
-
hash: "about",
|
|
100
|
-
},
|
|
101
|
-
],
|
|
59
|
+
const generalSearchEntries = useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
buildAnalyticsGeneralSettingsSearchEntries(
|
|
62
|
+
t,
|
|
63
|
+
!!replayStorageStatus.data?.configured,
|
|
64
|
+
),
|
|
102
65
|
[replayStorageStatus.data?.configured, t],
|
|
103
66
|
);
|
|
104
67
|
|
|
@@ -184,7 +147,7 @@ export default function Settings() {
|
|
|
184
147
|
</CardDescription>
|
|
185
148
|
</CardHeader>
|
|
186
149
|
<CardContent>
|
|
187
|
-
<ReplayStorageHint />
|
|
150
|
+
<ReplayStorageHint embedded />
|
|
188
151
|
</CardContent>
|
|
189
152
|
</Card>
|
|
190
153
|
) : null}
|