@robosystems/core 0.8.8 → 0.9.0

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.
@@ -4,6 +4,7 @@ export { EntitySelectorCore, type EntityLike, type EntityRecord, type EntitySele
4
4
  export { GraphFilters, byGraphType, composeFilters, excludeRepositories, excludeSubgraphs, hasAllSchemaExtensions, hasAnySchemaExtension, hasSchemaExtension, onlyEntityGraphs, onlyGenericGraphs, onlyRepositories, onlyUserGraphs, } from './graph-filters';
5
5
  export { GraphSelectorCore, type GraphSelectorProps } from './GraphSelectorCore';
6
6
  export { PageLayout } from './PageLayout';
7
+ export { ReportChat, anchoredMessage, errorDetail, type ReportChatProps, } from './report-chat';
7
8
  export { ActiveSubscriptions, BrowseRepositories, type ActiveSubscriptionsProps, type BrowseRepositoriesProps, } from './repositories';
8
9
  export { RepositoryGuard, useIsRepository } from './RepositoryGuard';
9
10
  export { SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, } from './search';
@@ -4,6 +4,7 @@ export { EntitySelectorCore, } from './EntitySelectorCore';
4
4
  export { GraphFilters, byGraphType, composeFilters, excludeRepositories, excludeSubgraphs, hasAllSchemaExtensions, hasAnySchemaExtension, hasSchemaExtension, onlyEntityGraphs, onlyGenericGraphs, onlyRepositories, onlyUserGraphs, } from './graph-filters';
5
5
  export { GraphSelectorCore } from './GraphSelectorCore';
6
6
  export { PageLayout } from './PageLayout';
7
+ export { ReportChat, anchoredMessage, errorDetail, } from './report-chat';
7
8
  export { ActiveSubscriptions, BrowseRepositories, } from './repositories';
8
9
  export { RepositoryGuard, useIsRepository } from './RepositoryGuard';
9
10
  export { SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, } from './search';
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface ReportChatProps {
3
+ /** The graph the operator runs on. */
4
+ graphId: string;
5
+ /** The note that anchors the operator on the report on screen. */
6
+ anchorNote: string;
7
+ /** The same report's identifiers, sent as `context.focus`. */
8
+ focus?: Record<string, unknown>;
9
+ /** Card title. */
10
+ title?: string;
11
+ /** Short text at the right of the header, e.g. what answers and what it costs. */
12
+ hint?: string;
13
+ /** The empty-state paragraph above the example questions. */
14
+ intro?: ReactNode;
15
+ /** Tap-to-ask questions on the empty state. */
16
+ examples?: string[];
17
+ placeholder?: string;
18
+ /**
19
+ * When false the body shows `unavailable` instead of the chat — for a user
20
+ * without access to the graph, say. The header still renders, so the card
21
+ * reads the same wherever it sits.
22
+ */
23
+ available?: boolean;
24
+ unavailable?: ReactNode;
25
+ defaultOpen?: boolean;
26
+ /** Operator mode; `standard` gives the loop enough tool turns for a schema read, a query or two, and an answer. */
27
+ mode?: 'quick' | 'standard' | 'extended';
28
+ }
29
+ export declare function ReportChat({ graphId, anchorNote, focus, title, hint, intro, examples, placeholder, available, unavailable, defaultOpen, mode, }: ReportChatProps): import("react").JSX.Element;
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Ask about this report — a collapsible chat card that sends questions to the
5
+ * RoboSystems operator on one graph, anchored on the report the host page
6
+ * has on screen. No key to enter: the call is the account's own, through the
7
+ * SDK's operator client (the same one the Console uses), and it spends the
8
+ * account's credits, which each answer shows.
9
+ *
10
+ * The host decides the anchor. RoboInvestor passes a filer and a filing on
11
+ * the SEC graph; RoboLedger passes an entity and a report on its own graph.
12
+ * Both hand the same shape: a note the operator reads ahead of every
13
+ * question, and a `focus` object with the same identifiers for an operator
14
+ * release that reads `context` directly.
15
+ */
16
+ import { Spinner } from 'flowbite-react';
17
+ import { useCallback, useRef, useState } from 'react';
18
+ import { HiChevronDown, HiChevronRight, HiSparkles } from 'react-icons/hi';
19
+ import ReactMarkdown from 'react-markdown';
20
+ import remarkGfm from 'remark-gfm';
21
+ import { anchoredMessage, errorDetail } from './anchor';
22
+ const MARKDOWN_CLASS = 'text-sm leading-relaxed text-zinc-800 dark:text-zinc-200 [&_a]:text-primary-600 dark:[&_a]:text-primary-400 [&_a]:underline [&_code]:rounded [&_code]:bg-zinc-100 [&_code]:px-1 dark:[&_code]:bg-zinc-800 [&_h1]:mt-3 [&_h1]:text-base [&_h1]:font-semibold [&_h2]:mt-3 [&_h2]:text-base [&_h2]:font-semibold [&_h3]:mt-2 [&_h3]:font-semibold [&_li]:my-0.5 [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:my-2 [&_strong]:font-semibold [&_table]:my-2 [&_table]:text-xs [&_td]:px-2 [&_td]:py-1 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_ul]:list-disc [&_ul]:pl-5';
23
+ export function ReportChat({ graphId, anchorNote, focus, title = 'Ask about this report', hint, intro, examples = [], placeholder = 'Ask…', available = true, unavailable, defaultOpen = false, mode = 'standard', }) {
24
+ const [open, setOpen] = useState(defaultOpen);
25
+ const [turns, setTurns] = useState([]);
26
+ const [draft, setDraft] = useState('');
27
+ const [busy, setBusy] = useState(false);
28
+ const [progress, setProgress] = useState('');
29
+ const scrollRef = useRef(null);
30
+ const ask = useCallback(async (question) => {
31
+ var _a;
32
+ const q = question.trim();
33
+ if (!q || busy)
34
+ return;
35
+ setTurns((prev) => [...prev, { role: 'user', text: q }]);
36
+ setBusy(true);
37
+ setProgress('Thinking');
38
+ try {
39
+ const { clients } = await import('@robosystems/client/clients');
40
+ // Prior turns as the operator saw them: the questions without the
41
+ // anchor, which rides on every current message, and the answers.
42
+ const history = turns.map((t) => ({ role: t.role, content: t.text }));
43
+ const result = await clients.operator.executeQuery(graphId, Object.assign(Object.assign({ message: anchoredMessage(anchorNote, q), mode }, (history.length > 0 ? { history } : {})), (focus ? { context: { focus } } : {})), {
44
+ mode: 'auto',
45
+ onProgress: (message, percentage) => setProgress(percentage !== undefined
46
+ ? `${message} (${percentage}%)`
47
+ : message),
48
+ });
49
+ // The API reports operator failures (credit pre-flight, timeouts) as
50
+ // a normal response with `error_details` set; render those as errors.
51
+ if (result.error_details) {
52
+ setTurns((prev) => [
53
+ ...prev,
54
+ {
55
+ role: 'assistant',
56
+ text: errorDetail(result.error_details, result.content ||
57
+ 'The operator could not complete this request.'),
58
+ error: true,
59
+ },
60
+ ]);
61
+ return;
62
+ }
63
+ const credits = ((_a = result.metadata) !== null && _a !== void 0 ? _a : {})['credits_consumed'];
64
+ setTurns((prev) => [
65
+ ...prev,
66
+ {
67
+ role: 'assistant',
68
+ text: result.content || '(no answer returned)',
69
+ credits: typeof credits === 'number' ? credits : null,
70
+ },
71
+ ]);
72
+ }
73
+ catch (e) {
74
+ setTurns((prev) => [
75
+ ...prev,
76
+ {
77
+ role: 'assistant',
78
+ text: `Error: ${e instanceof Error ? e.message : String(e)}`,
79
+ error: true,
80
+ },
81
+ ]);
82
+ }
83
+ finally {
84
+ setBusy(false);
85
+ setProgress('');
86
+ requestAnimationFrame(() => {
87
+ const el = scrollRef.current;
88
+ if (el)
89
+ el.scrollTop = el.scrollHeight;
90
+ });
91
+ }
92
+ }, [busy, turns, graphId, anchorNote, focus, mode]);
93
+ return (_jsxs("section", { className: "mb-8 rounded-lg border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900/60", children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, className: "flex w-full items-center gap-2 px-4 py-3 text-left", children: [open ? (_jsx(HiChevronDown, { className: "h-4 w-4 shrink-0 text-zinc-400" })) : (_jsx(HiChevronRight, { className: "h-4 w-4 shrink-0 text-zinc-400" })), _jsx(HiSparkles, { className: "text-primary-500 h-4 w-4 shrink-0" }), _jsx("span", { className: "font-semibold text-zinc-900 dark:text-white", children: title }), hint ? (_jsx("span", { className: "ml-auto hidden text-xs text-zinc-500 sm:inline dark:text-zinc-400", children: hint })) : null] }), open && !available ? (_jsx("div", { className: "border-t border-zinc-200 px-4 py-4 text-sm text-zinc-600 dark:border-zinc-800 dark:text-zinc-400", children: unavailable })) : null, open && available ? (_jsxs("div", { className: "border-t border-zinc-200 dark:border-zinc-800", children: [_jsxs("div", { ref: scrollRef, className: "max-h-[28rem] space-y-3 overflow-y-auto px-4 py-4", children: [turns.length === 0 ? (_jsxs("div", { className: "text-sm text-zinc-600 dark:text-zinc-400", children: [intro ? _jsx("p", { children: intro }) : null, examples.length > 0 ? (_jsx("div", { className: "mt-3 flex flex-wrap gap-2", children: examples.map((q) => (_jsx("button", { type: "button", disabled: busy, onClick: () => void ask(q), className: "rounded-full border border-zinc-300 px-3 py-1 text-xs text-zinc-700 hover:bg-zinc-50 disabled:opacity-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800", children: q }, q))) })) : null] })) : (turns.map((turn, i) => turn.role === 'user' ? (_jsx("div", { className: "ml-auto max-w-[85%] rounded-lg bg-zinc-100 px-3 py-2 text-sm text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100", children: turn.text }, i)) : (_jsxs("div", { className: `max-w-[95%] rounded-lg px-3 py-2 ${turn.error
94
+ ? 'border border-red-200 bg-red-50 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-900/20 dark:text-red-400'
95
+ : 'bg-zinc-50 dark:bg-zinc-950/40'}`, children: [turn.error ? (turn.text) : (_jsx("div", { className: MARKDOWN_CLASS, children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: turn.text }) })), typeof turn.credits === 'number' ? (_jsxs("div", { className: "mt-1 text-xs text-zinc-400", children: [turn.credits.toFixed(1), " credits"] })) : null] }, i)))), busy ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx(Spinner, { size: "sm" }), " ", progress || 'Thinking', "\u2026"] })) : null] }), _jsxs("form", { className: "flex gap-2 border-t border-zinc-200 px-4 py-3 dark:border-zinc-800", onSubmit: (e) => {
96
+ e.preventDefault();
97
+ const q = draft;
98
+ setDraft('');
99
+ void ask(q);
100
+ }, children: [_jsx("input", { type: "text", value: draft, onChange: (e) => setDraft(e.target.value), placeholder: placeholder, disabled: busy, "aria-label": title, className: "focus:border-primary-500 focus:ring-primary-500 flex-1 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder-zinc-500" }), _jsx("button", { type: "submit", disabled: busy || !draft.trim(), className: "bg-primary-600 hover:bg-primary-700 rounded-lg px-4 py-2 text-sm font-medium text-white disabled:opacity-50", children: "Ask" })] })] })) : null] }));
101
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The message sent for one question: the anchor note the host page wrote
3
+ * for the report on screen, then the question. The note rides on every
4
+ * message because the operator does not yet read `context.focus` into its
5
+ * prompt; when it does, hosts can pass `focus` alone and an empty note.
6
+ */
7
+ export declare function anchoredMessage(anchorNote: string, question: string): string;
8
+ /** The first non-empty string among an operator failure's fields. */
9
+ export declare function errorDetail(details: Record<string, unknown> | undefined, fallback: string): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The message sent for one question: the anchor note the host page wrote
3
+ * for the report on screen, then the question. The note rides on every
4
+ * message because the operator does not yet read `context.focus` into its
5
+ * prompt; when it does, hosts can pass `focus` alone and an empty note.
6
+ */
7
+ export function anchoredMessage(anchorNote, question) {
8
+ const q = question.trim();
9
+ const note = anchorNote.trim();
10
+ return note ? `${note}\n\nQuestion: ${q}` : q;
11
+ }
12
+ /** The first non-empty string among an operator failure's fields. */
13
+ export function errorDetail(details, fallback) {
14
+ if (!details)
15
+ return fallback;
16
+ const found = [details.message, details.error, details.detail].find((v) => typeof v === 'string' && v.length > 0);
17
+ return found !== null && found !== void 0 ? found : fallback;
18
+ }
@@ -0,0 +1,2 @@
1
+ export { anchoredMessage, errorDetail } from './anchor';
2
+ export { ReportChat, type ReportChatProps } from './ReportChat';
@@ -0,0 +1,2 @@
1
+ export { anchoredMessage, errorDetail } from './anchor';
2
+ export { ReportChat } from './ReportChat';
package/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export type { Entities, Entity, User } from './types';
12
12
  export { customTheme } from './theme';
13
13
  export { buildGraphAwareConsoleConfig, ConsoleContent, EXAMPLE_SETS, getGraphExampleKind, useGraphAwareConsoleConfig, } from './components/console';
14
14
  export type { ConsoleBranding, ConsoleCommandContext, ConsoleConfig, ConsoleExtraCommand, ConsoleHeaderConfig, ConsoleMcpConfig, ConsoleWelcomeConfig, GraphExampleKind, GraphExampleSet, SampleQuery, } from './components/console';
15
+ export { anchoredMessage, errorDetail, ReportChat, type ReportChatProps, } from './components/report-chat';
15
16
  export { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, useIsRepository, type ActiveSubscriptionsProps, type BrowseRepositoriesProps, type EntityGroup, type EntityLike, type EntityRecord, type EntitySelectorProps, type GraphSelectorProps, type GraphWithEntities, type SearchBarProps, type SearchConfig, type SearchFilterConfig, type SearchHitCardProps, type SearchPaginationProps, type SearchResultsMetaProps, type SelectableEntity, } from './components';
16
17
  export { composeFilters, excludeRepositories, excludeSubgraphs, GraphFilters, hasSchemaExtension, onlyRepositories, onlyUserGraphs, } from './components/graph-filters';
17
18
  export { ApiKeyDisplay, BrandSpinner, categorizeError, CategoryInput, ChatHeader, ChatInputArea, ChatMessage, ConfirmModal, CoreNavbar, CoreSidebar, CreateApiKeyModal, DeepResearchToggle, EmptyState, ErrorType, GeneralInformationCard, generateMessageId, getErrorMessage, LandingFooter, LoadingState, MarkdownProse, PageContainer, PageHeader, PasswordInformationCard, PasswordRequirements, SecureApiKeyField, SettingsCard, SettingsContainer, SettingsFormField, SettingsPageHeader, Spinner, StatCard, StatusAlert, SupportModal, TagInput, ThemeToggle, } from './ui-components';
package/index.js CHANGED
@@ -31,6 +31,7 @@ export { customTheme } from './theme';
31
31
  // Export components
32
32
  // Export console components
33
33
  export { buildGraphAwareConsoleConfig, ConsoleContent, EXAMPLE_SETS, getGraphExampleKind, useGraphAwareConsoleConfig, } from './components/console';
34
+ export { anchoredMessage, errorDetail, ReportChat, } from './components/report-chat';
34
35
  export { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, useIsRepository, } from './components';
35
36
  // Export graph filters
36
37
  export { composeFilters, excludeRepositories, excludeSubgraphs, GraphFilters, hasSchemaExtension, onlyRepositories, onlyUserGraphs, } from './components/graph-filters';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -11,7 +11,8 @@ export interface MarkdownProseProps {
11
11
  * search results). Theme-aware via `dark:prose-dark` — the consuming apps
12
12
  * define the `typography.dark` variant in their Tailwind configs; do NOT
13
13
  * switch to `dark:prose-invert`, which doesn't resolve under their
14
- * Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
14
+ * Tailwind v4 + typography-plugin setup (first hit in the research article,
15
+ * now roboinvestor-app's src/components/research/ResearchArticle.tsx).
15
16
  * Works in server and client components alike.
16
17
  */
17
18
  export declare function MarkdownProse({ children, size, className, }: MarkdownProseProps): import("react").JSX.Element;
@@ -7,7 +7,8 @@ import { twMerge } from 'tailwind-merge';
7
7
  * search results). Theme-aware via `dark:prose-dark` — the consuming apps
8
8
  * define the `typography.dark` variant in their Tailwind configs; do NOT
9
9
  * switch to `dark:prose-invert`, which doesn't resolve under their
10
- * Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
10
+ * Tailwind v4 + typography-plugin setup (first hit in the research article,
11
+ * now roboinvestor-app's src/components/research/ResearchArticle.tsx).
11
12
  * Works in server and client components alike.
12
13
  */
13
14
  export function MarkdownProse({ children, size = 'base', className, }) {
@@ -43,5 +43,5 @@ export function LandingFooter({ tagline, productLinks, contactModal: ContactModa
43
43
  return (_jsx("li", { children: _jsx(Link, Object.assign({ href: cfg.url }, (isCurrent
44
44
  ? {}
45
45
  : { target: '_blank', rel: 'noopener noreferrer' }), { className: linkClass, children: cfg.displayName })) }, app));
46
- }) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: researchHref }, researchLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://holon.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Holon Viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: resolvedBlogHref }, blogLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
46
+ }) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: researchHref }, researchLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://xbrlkit.com", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "xbrlkit viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: resolvedBlogHref }, blogLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
47
47
  }
@@ -1,19 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- export interface CoverageBrowserProps {
3
- items: CoverageItem[];
4
- /** Base path for card links, forwarded to the grid (defaults to /research). */
5
- hrefBase?: string;
6
- /** Override the search box placeholder. */
7
- placeholder?: string;
8
- }
9
- /**
10
- * The research listing with a live client-side filter above it. A drop-in
11
- * replacement for {@link CoverageGrid}: the full grid renders on first paint
12
- * (so the server-rendered SEO page keeps every card in its HTML), and typing
13
- * narrows it in the browser. Matches ticker, company, title, and tags —
14
- * someone who types "Green Thumb" finds GTBIF without knowing the symbol.
15
- *
16
- * Filtering the whole in-memory catalog is fine at this scale; when the
17
- * catalog outgrows a single fetch this is where server-backed search slots in.
18
- */
19
- export declare function CoverageBrowser({ items, hrefBase, placeholder, }: CoverageBrowserProps): import("react").JSX.Element;
@@ -1,35 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { TextInput } from 'flowbite-react';
4
- import { useMemo, useState } from 'react';
5
- import { HiSearch } from 'react-icons/hi';
6
- import { CoverageGrid } from './CoverageGrid';
7
- /**
8
- * The research listing with a live client-side filter above it. A drop-in
9
- * replacement for {@link CoverageGrid}: the full grid renders on first paint
10
- * (so the server-rendered SEO page keeps every card in its HTML), and typing
11
- * narrows it in the browser. Matches ticker, company, title, and tags —
12
- * someone who types "Green Thumb" finds GTBIF without knowing the symbol.
13
- *
14
- * Filtering the whole in-memory catalog is fine at this scale; when the
15
- * catalog outgrows a single fetch this is where server-backed search slots in.
16
- */
17
- export function CoverageBrowser({ items, hrefBase, placeholder = 'Search by ticker, company, or title…', }) {
18
- const [query, setQuery] = useState('');
19
- const filtered = useMemo(() => {
20
- const term = query.trim().toLowerCase();
21
- if (!term)
22
- return items;
23
- return items.filter((item) => item.ticker.toLowerCase().includes(term) ||
24
- item.company.toLowerCase().includes(term) ||
25
- item.title.toLowerCase().includes(term) ||
26
- item.tags.some((tag) => tag.toLowerCase().includes(term)));
27
- }, [items, query]);
28
- // Empty catalog: let the grid render its own "no coverage yet" state, no
29
- // point showing a search box over nothing.
30
- if (!items.length) {
31
- return _jsx(CoverageGrid, { items: items, hrefBase: hrefBase });
32
- }
33
- const term = query.trim();
34
- return (_jsxs("div", { className: "space-y-6", children: [_jsx(TextInput, { icon: HiSearch, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: placeholder, "aria-label": "Search research" }), term && filtered.length === 0 ? (_jsxs("p", { className: "py-12 text-center text-gray-500 dark:text-gray-400", children: ["No results for \u201C", term, "\u201D."] })) : (_jsx(CoverageGrid, { items: filtered, hrefBase: hrefBase }))] }));
35
- }
@@ -1,12 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /**
3
- * A coverage tile: full-bleed thumbnail on top, then ticker/label, title, summary, and
4
- * prior-report count. Self-contained (not the shared Flowbite Card) so the cover image
5
- * sits flush at the top of every card regardless of how much text each one has — the
6
- * shared card theme centers its content (`justify-center`), which left the covers at
7
- * different heights across a row.
8
- */
9
- export declare function CoverageCard({ item, hrefBase, }: {
10
- item: CoverageItem;
11
- hrefBase?: string;
12
- }): import("react").JSX.Element;
@@ -1,13 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import Link from 'next/link';
3
- /**
4
- * A coverage tile: full-bleed thumbnail on top, then ticker/label, title, summary, and
5
- * prior-report count. Self-contained (not the shared Flowbite Card) so the cover image
6
- * sits flush at the top of every card regardless of how much text each one has — the
7
- * shared card theme centers its content (`justify-center`), which left the covers at
8
- * different heights across a row.
9
- */
10
- export function CoverageCard({ item, hrefBase = '/research', }) {
11
- var _a;
12
- return (_jsx(Link, { href: `${hrefBase}/${item.ticker.toLowerCase()}`, className: "group block h-full", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white/80 shadow-lg backdrop-blur-sm transition-shadow duration-300 hover:shadow-xl dark:border-gray-700 dark:bg-zinc-950", children: [item.assets.thumbnail && (_jsx("img", { src: item.assets.thumbnail, alt: item.title, className: "aspect-video w-full object-cover" })), _jsxs("div", { className: "flex flex-1 flex-col gap-2 p-5", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400", children: [_jsx("span", { className: "rounded bg-cyan-500/10 px-2 py-0.5 font-semibold text-cyan-600 dark:text-cyan-400", children: item.ticker }), item.coverage_label && _jsx("span", { children: item.coverage_label }), _jsx("span", { className: "ml-auto", children: (_a = item.date) === null || _a === void 0 ? void 0 : _a.slice(0, 10) })] }), _jsx("h3", { className: "text-lg font-bold text-gray-900 group-hover:text-cyan-600 dark:text-white dark:group-hover:text-cyan-400", children: item.title }), _jsx("p", { className: "line-clamp-3 text-sm text-gray-600 dark:text-gray-300", children: item.summary }), item.history.length > 0 && (_jsxs("p", { className: "mt-auto pt-1 text-xs text-gray-400", children: ["+", item.history.length, " prior", ' ', item.history.length === 1 ? 'report' : 'reports'] }))] })] }) }));
13
- }
@@ -1,6 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /** Responsive grid of coverage cards. */
3
- export declare function CoverageGrid({ items, hrefBase, }: {
4
- items: CoverageItem[];
5
- hrefBase?: string;
6
- }): import("react").JSX.Element;
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { CoverageCard } from './CoverageCard';
3
- /** Responsive grid of coverage cards. */
4
- export function CoverageGrid({ items, hrefBase, }) {
5
- if (!items.length) {
6
- return (_jsx("p", { className: "py-12 text-center text-gray-500 dark:text-gray-400", children: "No coverage yet \u2014 check back soon." }));
7
- }
8
- return (_jsx("div", { className: "grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3", children: items.map((item) => (_jsx(CoverageCard, { item: item, hrefBase: hrefBase }, item.ticker))) }));
9
- }
@@ -1,5 +0,0 @@
1
- import type { CoverageVersion } from './types';
2
- /** The continuing-coverage timeline: prior dated reports with links to their archived assets. */
3
- export declare function CoverageHistory({ history }: {
4
- history: CoverageVersion[];
5
- }): import("react").JSX.Element;
@@ -1,7 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /** The continuing-coverage timeline: prior dated reports with links to their archived assets. */
3
- export function CoverageHistory({ history }) {
4
- if (!history.length)
5
- return null;
6
- return (_jsxs("section", { className: "mt-12 border-t border-gray-200 pt-8 dark:border-gray-800", children: [_jsx("h2", { className: "mb-4 text-xl font-bold text-gray-900 dark:text-white", children: "Coverage history" }), _jsx("ol", { className: "space-y-3", children: history.map((h) => (_jsxs("li", { className: "flex flex-col gap-1 rounded-lg border border-gray-200 p-4 sm:flex-row sm:items-center sm:gap-4 dark:border-gray-800", children: [_jsx("span", { className: "shrink-0 rounded bg-gray-100 px-2 py-0.5 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: h.version }), _jsxs("span", { className: "flex-1 text-sm text-gray-700 dark:text-gray-200", children: [h.title || 'Prior report', h.legacy_ticker && (_jsxs("em", { className: "text-gray-400", children: [" (as ", h.legacy_ticker, ")"] }))] }), _jsxs("span", { className: "flex gap-3 text-xs", children: [(h.youtube_url || h.assets.video) && (_jsx("a", { href: h.youtube_url || h.assets.video, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Video" })), h.assets.podcast_mp3 && (_jsx("a", { href: h.assets.podcast_mp3, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Podcast" })), h.assets.brief && (_jsx("a", { href: h.assets.brief, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Brief" }))] })] }, h.version))) })] }));
7
- }
@@ -1,16 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /**
3
- * The full coverage report: native video, the Q&A podcast (surfaced as a prominent
4
- * "Listen" card right under the video), then the brief rendered from markdown (its own
5
- * leading H1 is stripped — we render the title above it), and the continuing-coverage
6
- * history. Works in a server component (SSR'd for SEO) or a client one.
7
- * Theme-aware: readable in both light and dark. The prose body sets an explicit
8
- * light + `dark:` color for every element it renders (headings, p, strong, em, links,
9
- * lists, code, blockquote, hr, tables) rather than relying on `prose-invert` /
10
- * `dark:prose-invert` — the latter doesn't resolve from a `.dark` class in the
11
- * Tailwind v4 + typography-plugin setup these apps use.
12
- */
13
- export declare function ResearchArticle({ item, briefMarkdown, }: {
14
- item: CoverageItem;
15
- briefMarkdown?: string;
16
- }): import("react").JSX.Element;
@@ -1,25 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import ReactMarkdown from 'react-markdown';
3
- import remarkGfm from 'remark-gfm';
4
- import { youtubeId } from './catalog';
5
- import { CoverageHistory } from './CoverageHistory';
6
- /**
7
- * The full coverage report: native video, the Q&A podcast (surfaced as a prominent
8
- * "Listen" card right under the video), then the brief rendered from markdown (its own
9
- * leading H1 is stripped — we render the title above it), and the continuing-coverage
10
- * history. Works in a server component (SSR'd for SEO) or a client one.
11
- * Theme-aware: readable in both light and dark. The prose body sets an explicit
12
- * light + `dark:` color for every element it renders (headings, p, strong, em, links,
13
- * lists, code, blockquote, hr, tables) rather than relying on `prose-invert` /
14
- * `dark:prose-invert` — the latter doesn't resolve from a `.dark` class in the
15
- * Tailwind v4 + typography-plugin setup these apps use.
16
- */
17
- export function ResearchArticle({ item, briefMarkdown, }) {
18
- var _a;
19
- const body = (briefMarkdown || '').replace(/^#\s.*(\r?\n)+/, '');
20
- const ytId = youtubeId(item.youtube_url);
21
- const podcastYtId = youtubeId(item.podcast_youtube_url);
22
- return (_jsxs("article", { className: "mx-auto max-w-3xl", children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400", children: [_jsxs("span", { className: "rounded bg-cyan-500/10 px-2 py-0.5 font-semibold text-cyan-600 dark:text-cyan-400", children: [item.company, " \u00B7 ", item.ticker] }), item.coverage_label && _jsx("span", { children: item.coverage_label }), _jsx("span", { children: (_a = item.date) === null || _a === void 0 ? void 0 : _a.slice(0, 10) })] }), _jsx("h1", { className: "mb-6 text-3xl font-bold text-gray-900 dark:text-white", children: item.title }), ytId ? (_jsx("div", { className: "mb-8 aspect-video w-full overflow-hidden rounded-xl bg-black", children: _jsx("iframe", { className: "h-full w-full", src: `https://www.youtube.com/embed/${ytId}`, title: item.title, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true }) })) : (item.assets.video && (_jsx("video", { controls: true, poster: item.assets.thumbnail, src: item.assets.video, className: "mb-8 aspect-video w-full rounded-xl bg-black" }))), (item.assets.podcast_mp3 || podcastYtId) && (_jsx("section", { className: "mb-8", children: item.assets.podcast_mp3 ? (_jsxs("div", { className: "rounded-xl border border-cyan-500/30 bg-cyan-50/60 p-4 dark:bg-gray-900/50", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold text-cyan-600 dark:text-cyan-400", children: "\uD83C\uDF99 Listen \u2014 Q&A podcast" }), podcastYtId && (_jsx("a", { href: item.podcast_youtube_url, target: "_blank", rel: "noopener noreferrer", className: "shrink-0 text-xs text-cyan-600 hover:underline dark:text-cyan-400", children: "Watch on YouTube \u2197" }))] }), _jsx("audio", { controls: true, preload: "none", src: item.assets.podcast_mp3, className: "w-full" })] })) : (
23
- // no MP3 yet — fall back to the YouTube video player
24
- _jsxs(_Fragment, { children: [_jsx("h2", { className: "mb-3 text-xl font-bold text-gray-900 dark:text-white", children: "Listen \u2014 Q&A podcast" }), _jsx("div", { className: "aspect-video w-full overflow-hidden rounded-xl bg-black", children: _jsx("iframe", { className: "h-full w-full", src: `https://www.youtube.com/embed/${podcastYtId}`, title: `${item.title} — Q&A podcast`, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true }) })] })) })), body && (_jsx("div", { className: "prose prose-lg prose-headings:font-heading prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-a:text-cyan-600 dark:prose-a:text-cyan-400 prose-a:no-underline hover:prose-a:text-cyan-500 dark:hover:prose-a:text-cyan-300 prose-strong:text-gray-900 dark:prose-strong:text-white prose-strong:font-semibold prose-em:text-gray-700 dark:prose-em:text-gray-300 prose-code:text-cyan-700 dark:prose-code:text-cyan-400 prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-2 prose-code:py-1 prose-code:rounded prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-800 prose-blockquote:border-l-cyan-500 prose-blockquote:text-gray-600 dark:prose-blockquote:text-gray-400 prose-blockquote:italic prose-ul:text-gray-700 dark:prose-ul:text-gray-300 prose-ol:text-gray-700 dark:prose-ol:text-gray-300 prose-li:marker:text-cyan-500 prose-hr:border-gray-200 dark:prose-hr:border-gray-800 prose-table:border-gray-300 dark:prose-table:border-gray-700 prose-th:bg-gray-100 dark:prose-th:bg-gray-900 prose-th:text-gray-900 dark:prose-th:text-white prose-td:text-gray-700 dark:prose-td:text-gray-300 max-w-none", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: body }) })), _jsx(CoverageHistory, { history: item.history }), _jsxs("footer", { className: "mt-12 border-t border-gray-200 pt-6 dark:border-gray-800", children: [_jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Audio & music produced with ElevenLabs." }), _jsxs("a", { href: "https://elevenlabs.io/startup-grants", target: "_blank", rel: "noopener noreferrer", "aria-label": "Backed by the ElevenLabs Grants program", className: "mt-3 inline-block", children: [_jsx("img", { src: "/images/logos/elevenlabs-grants.webp", alt: "Backed by the ElevenLabs Grants program", width: 200, className: "h-auto dark:hidden" }), _jsx("img", { src: "/images/logos/elevenlabs-grants-white.webp", alt: "Backed by the ElevenLabs Grants program", width: 200, className: "hidden h-auto dark:block" })] }), _jsxs("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: ["Using ElevenLabs yourself? Our", ' ', _jsx("a", { href: "https://try.elevenlabs.io/v9z3wzm97gk3", target: "_blank", rel: "noopener noreferrer", className: "hover:underline", children: "referral link" }), ' ', "costs you nothing extra and supports this research."] })] })] }));
25
- }
@@ -1,14 +0,0 @@
1
- import type { Catalog, CoverageItem } from './types';
2
- export declare const CATALOG_URL: string;
3
- /** Fetch the full catalog. `revalidate` (seconds) drives Next ISR on the server. */
4
- export declare function fetchCatalog(revalidate?: number): Promise<Catalog>;
5
- /** All coverage items, newest first (the catalog is already sorted, but be defensive). */
6
- export declare function getAllCoverage(revalidate?: number): Promise<CoverageItem[]>;
7
- /** One company's coverage (latest + history), or null if not in the catalog. */
8
- export declare function getCoverage(ticker: string, revalidate?: number): Promise<CoverageItem | null>;
9
- /** Tickers for generateStaticParams(). */
10
- export declare function getCoverageTickers(): Promise<string[]>;
11
- /** Fetch a brief's raw markdown (rendered to HTML by ResearchArticle). */
12
- export declare function fetchBrief(url: string, revalidate?: number): Promise<string>;
13
- /** Extract a YouTube video id from a youtu.be / watch?v= / embed URL (or null). */
14
- export declare function youtubeId(url?: string | null): string | null;
@@ -1,42 +0,0 @@
1
- // Data access for the research catalog. The catalog is a single public JSON file served
2
- // via the CloudFront CDN, produced by `just reindex` in robosystems-content-machine.
3
- // Fetched server-side (SSG/ISR) in the public site and client-side in the logged-in app.
4
- export const CATALOG_URL = process.env.NEXT_PUBLIC_RESEARCH_CATALOG_URL ||
5
- 'https://assets.robosystems.ai/content/index.json';
6
- /** Fetch the full catalog. `revalidate` (seconds) drives Next ISR on the server. */
7
- export async function fetchCatalog(revalidate = 3600) {
8
- const res = await fetch(CATALOG_URL, { next: { revalidate } });
9
- if (!res.ok)
10
- throw new Error(`Research catalog fetch failed: ${res.status}`);
11
- return (await res.json());
12
- }
13
- /** All coverage items, newest first (the catalog is already sorted, but be defensive). */
14
- export async function getAllCoverage(revalidate = 3600) {
15
- const { items } = await fetchCatalog(revalidate);
16
- return [...items].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
17
- }
18
- /** One company's coverage (latest + history), or null if not in the catalog. */
19
- export async function getCoverage(ticker, revalidate = 3600) {
20
- const { items } = await fetchCatalog(revalidate);
21
- const t = ticker.toUpperCase();
22
- return items.find((i) => i.ticker.toUpperCase() === t) || null;
23
- }
24
- /** Tickers for generateStaticParams(). */
25
- export async function getCoverageTickers() {
26
- const { items } = await fetchCatalog();
27
- return items.map((i) => i.ticker);
28
- }
29
- /** Fetch a brief's raw markdown (rendered to HTML by ResearchArticle). */
30
- export async function fetchBrief(url, revalidate = 3600) {
31
- const res = await fetch(url, { next: { revalidate } });
32
- if (!res.ok)
33
- throw new Error(`Brief fetch failed: ${res.status}`);
34
- return await res.text();
35
- }
36
- /** Extract a YouTube video id from a youtu.be / watch?v= / embed URL (or null). */
37
- export function youtubeId(url) {
38
- if (!url)
39
- return null;
40
- const m = url.match(/(?:youtu\.be\/|[?&]v=|embed\/)([A-Za-z0-9_-]{6,})/);
41
- return m ? m[1] : null;
42
- }
@@ -1,7 +0,0 @@
1
- export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
2
- export { CoverageBrowser, type CoverageBrowserProps } from './CoverageBrowser';
3
- export { CoverageCard } from './CoverageCard';
4
- export { CoverageGrid } from './CoverageGrid';
5
- export { CoverageHistory } from './CoverageHistory';
6
- export { ResearchArticle } from './ResearchArticle';
7
- export * from './types';
package/research/index.js DELETED
@@ -1,10 +0,0 @@
1
- // Research portal module — shared across robosystems-app (public/SEO) and
2
- // roboinvestor-app (logged-in). Data layer reads the S3 catalog produced by
3
- // robosystems-content-machine; components render the coverage + continuing-coverage thread.
4
- export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
5
- export { CoverageBrowser } from './CoverageBrowser';
6
- export { CoverageCard } from './CoverageCard';
7
- export { CoverageGrid } from './CoverageGrid';
8
- export { CoverageHistory } from './CoverageHistory';
9
- export { ResearchArticle } from './ResearchArticle';
10
- export * from './types';
@@ -1,38 +0,0 @@
1
- export interface CoverageAssets {
2
- video?: string;
3
- short?: string;
4
- podcast_mp3?: string;
5
- podcast_mp4?: string;
6
- brief?: string;
7
- thumbnail?: string;
8
- }
9
- export interface CoverageVersion {
10
- version: string;
11
- date?: string;
12
- title?: string;
13
- legacy_ticker?: string;
14
- youtube_url?: string;
15
- assets: CoverageAssets;
16
- }
17
- export interface CoverageItem {
18
- ticker: string;
19
- company: string;
20
- title: string;
21
- summary: string;
22
- tags: string[];
23
- campaign?: string | null;
24
- campaign_slug?: string | null;
25
- coverage_label?: string | null;
26
- date: string;
27
- version: string;
28
- youtube_url?: string;
29
- short_youtube_url?: string;
30
- podcast_youtube_url?: string;
31
- assets: CoverageAssets;
32
- history: CoverageVersion[];
33
- }
34
- export interface Catalog {
35
- generated: string;
36
- count: number;
37
- items: CoverageItem[];
38
- }
package/research/types.js DELETED
@@ -1,4 +0,0 @@
1
- // Shared types for the equity-research catalog produced by robosystems-content-machine
2
- // (https://assets.robosystems.ai/content/index.json). The company is the durable
3
- // entity; each run is a dated quarterly report version (latest + history[]).
4
- export {};