@robosystems/core 0.4.0 → 0.5.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/components/index.d.ts +2 -2
- package/components/index.js +1 -1
- package/components/search/SearchBar.d.ts +21 -0
- package/components/search/SearchBar.js +16 -0
- package/components/search/SearchContent.js +7 -24
- package/components/search/SearchHitCard.d.ts +25 -0
- package/components/search/SearchHitCard.js +15 -0
- package/components/search/SearchPagination.d.ts +13 -0
- package/components/search/SearchPagination.js +12 -0
- package/components/search/SearchResultsMeta.d.ts +16 -0
- package/components/search/SearchResultsMeta.js +9 -0
- package/components/search/index.d.ts +4 -0
- package/components/search/index.js +4 -0
- package/index.d.ts +3 -2
- package/index.js +3 -3
- package/package.json +1 -1
- package/research/CoverageBrowser.d.ts +19 -0
- package/research/CoverageBrowser.js +35 -0
- package/research/index.d.ts +1 -0
- package/research/index.js +1 -0
- package/ui-components/MarkdownProse.d.ts +17 -0
- package/ui-components/MarkdownProse.js +17 -0
- package/ui-components/forms/CategoryInput.d.ts +24 -0
- package/ui-components/forms/CategoryInput.js +69 -0
- package/ui-components/forms/TagInput.d.ts +25 -0
- package/ui-components/forms/TagInput.js +68 -0
- package/ui-components/forms/index.d.ts +2 -0
- package/ui-components/forms/index.js +2 -0
- package/ui-components/index.d.ts +1 -0
- package/ui-components/index.js +1 -0
package/components/index.d.ts
CHANGED
|
@@ -6,5 +6,5 @@ export { GraphSelectorCore, type GraphSelectorProps } from './GraphSelectorCore'
|
|
|
6
6
|
export { PageLayout } from './PageLayout';
|
|
7
7
|
export { ActiveSubscriptions, BrowseRepositories, type ActiveSubscriptionsProps, type BrowseRepositoriesProps, } from './repositories';
|
|
8
8
|
export { RepositoryGuard, useIsRepository } from './RepositoryGuard';
|
|
9
|
-
export { SearchContent } from './search';
|
|
10
|
-
export type { SearchConfig, SearchFilterConfig } from './search';
|
|
9
|
+
export { SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, } from './search';
|
|
10
|
+
export type { SearchBarProps, SearchConfig, SearchFilterConfig, SearchHitCardProps, SearchPaginationProps, SearchResultsMetaProps, } from './search';
|
package/components/index.js
CHANGED
|
@@ -6,4 +6,4 @@ export { GraphSelectorCore } from './GraphSelectorCore';
|
|
|
6
6
|
export { PageLayout } from './PageLayout';
|
|
7
7
|
export { ActiveSubscriptions, BrowseRepositories, } from './repositories';
|
|
8
8
|
export { RepositoryGuard, useIsRepository } from './RepositoryGuard';
|
|
9
|
-
export { SearchContent } from './search';
|
|
9
|
+
export { SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, } from './search';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SearchBarProps {
|
|
2
|
+
query: string;
|
|
3
|
+
onQueryChange: (query: string) => void;
|
|
4
|
+
/** Invoked by both the Enter key and the search button. */
|
|
5
|
+
onSearch: () => void;
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Button label — Recall surfaces pass 'Recall'. */
|
|
9
|
+
buttonLabel?: string;
|
|
10
|
+
buttonColor?: string;
|
|
11
|
+
/** When provided, a gray X clear button renders after the search button. */
|
|
12
|
+
onClear?: () => void;
|
|
13
|
+
/** Controls clear-button visibility (e.g. only after a search ran). */
|
|
14
|
+
showClear?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The standard search input row: query field + submit button + optional
|
|
18
|
+
* clear button. Shared by the document Search page and the memory Recall
|
|
19
|
+
* panel so the two surfaces stay visually aligned.
|
|
20
|
+
*/
|
|
21
|
+
export declare function SearchBar({ query, onQueryChange, onSearch, loading, placeholder, buttonLabel, buttonColor, onClear, showClear, }: SearchBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Spinner, TextInput } from 'flowbite-react';
|
|
4
|
+
import { HiSearch, HiX } from 'react-icons/hi';
|
|
5
|
+
/**
|
|
6
|
+
* The standard search input row: query field + submit button + optional
|
|
7
|
+
* clear button. Shared by the document Search page and the memory Recall
|
|
8
|
+
* panel so the two surfaces stay visually aligned.
|
|
9
|
+
*/
|
|
10
|
+
export function SearchBar({ query, onQueryChange, onSearch, loading = false, placeholder, buttonLabel = 'Search', buttonColor = 'purple', onClear, showClear, }) {
|
|
11
|
+
const clearVisible = onClear ? (showClear !== null && showClear !== void 0 ? showClear : true) : false;
|
|
12
|
+
return (_jsxs("div", { className: "flex gap-2", children: [_jsx(TextInput, { className: "flex-1", icon: HiSearch, value: query, onChange: (e) => onQueryChange(e.target.value), onKeyDown: (e) => {
|
|
13
|
+
if (e.key === 'Enter')
|
|
14
|
+
onSearch();
|
|
15
|
+
}, placeholder: placeholder }), _jsx(Button, { color: buttonColor, onClick: onSearch, disabled: loading || !query.trim(), children: loading ? _jsx(Spinner, { size: "sm", className: "text-white" }) : buttonLabel }), clearVisible && (_jsx(Button, { color: "gray", onClick: onClear, "aria-label": "Clear search", children: _jsx(HiX, { className: "h-4 w-4" }) }))] }));
|
|
16
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as SDK from '@robosystems/client';
|
|
4
|
-
import { Alert,
|
|
4
|
+
import { Alert, Card, Select, Spinner, TextInput, ToggleSwitch, } from 'flowbite-react';
|
|
5
5
|
import { useCallback, useEffect, useState } from 'react';
|
|
6
6
|
import { HiChevronDown, HiChevronUp, HiSearch } from 'react-icons/hi';
|
|
7
|
-
import ReactMarkdown from 'react-markdown';
|
|
8
|
-
import remarkGfm from 'remark-gfm';
|
|
9
7
|
import { useIsRepository } from '../../components/RepositoryGuard';
|
|
10
8
|
import { useGraphContext } from '../../contexts';
|
|
9
|
+
import { MarkdownProse } from '../../ui-components/MarkdownProse';
|
|
11
10
|
import { PageLayout } from '../PageLayout';
|
|
11
|
+
import { SearchBar } from './SearchBar';
|
|
12
|
+
import { SearchHitCard } from './SearchHitCard';
|
|
13
|
+
import { SearchPagination } from './SearchPagination';
|
|
14
|
+
import { SearchResultsMeta } from './SearchResultsMeta';
|
|
12
15
|
const PAGE_SIZE = 20;
|
|
13
16
|
export function SearchContent({ config }) {
|
|
14
17
|
var _a, _b, _c;
|
|
@@ -110,11 +113,6 @@ export function SearchContent({ config }) {
|
|
|
110
113
|
setLoading(false);
|
|
111
114
|
}
|
|
112
115
|
}, [graphId, query, sourceType, entity, formType, fiscalYear, semantic]);
|
|
113
|
-
const handleKeyDown = (e) => {
|
|
114
|
-
if (e.key === 'Enter') {
|
|
115
|
-
handleSearch(0);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
116
|
const handleExpand = async (docId) => {
|
|
119
117
|
if (expandedDocId === docId) {
|
|
120
118
|
setExpandedDocId(null);
|
|
@@ -149,20 +147,5 @@ export function SearchContent({ config }) {
|
|
|
149
147
|
if (!graphId) {
|
|
150
148
|
return (_jsx(PageLayout, { children: _jsxs("div", { className: "py-12 text-center", children: [_jsx("div", { className: "from-primary-500 to-secondary-600 mx-auto mb-4 w-fit rounded-lg bg-gradient-to-br p-3", children: _jsx(HiSearch, { className: "h-8 w-8 text-white" }) }), _jsx("h2", { className: "font-heading text-lg font-semibold text-gray-700 dark:text-gray-300", children: config.title }), _jsx("p", { className: "mt-2 text-gray-500 dark:text-gray-400", children: "Select a graph to search documents." })] }) }));
|
|
151
149
|
}
|
|
152
|
-
return (_jsxs(PageLayout, { children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "from-primary-500 to-secondary-600 rounded-lg bg-gradient-to-br p-3", children: _jsx(HiSearch, { className: "h-8 w-8 text-white" }) }), _jsxs("div", { children: [_jsx("h1", { className: "font-heading text-3xl font-bold text-gray-900 dark:text-white", children: config.title }), _jsxs("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [config.description, docCount !== null && docCount > 0 && (_jsxs("span", { className: "ml-2 text-gray-400 dark:text-gray-500", children: ["(", docCount, " document", docCount !== 1 ? 's' : '', " indexed)"] }))] })] })] }), _jsx(Card, { children: _jsxs("div", { className: "space-y-4", children: [
|
|
153
|
-
var _a;
|
|
154
|
-
return (_jsxs(Card, { children: [_jsx("button", { type: "button", className: "w-full cursor-pointer text-left", onClick: () => handleExpand(hit.document_id), "aria-expanded": expandedDocId === hit.document_id, children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h3", { className: "text-base font-semibold text-gray-900 dark:text-white", children: hit.document_title || hit.section_label || 'Untitled' }), hit.section_label && hit.document_title && (_jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: ["/ ", hit.section_label] }))] }), _jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [_jsx(Badge, { color: "info", size: "xs", children: hit.score.toFixed(2) }), _jsx(Badge, { color: "gray", size: "xs", children: hit.source_type }), hit.entity_ticker && (_jsx(Badge, { color: "purple", size: "xs", children: hit.entity_ticker })), hit.form_type && (_jsx(Badge, { color: "warning", size: "xs", children: hit.form_type })), hit.fiscal_year && (_jsxs(Badge, { color: "gray", size: "xs", children: ["FY", hit.fiscal_year] })), (_a = hit.tags) === null || _a === void 0 ? void 0 : _a.map((tag) => (_jsx(Badge, { color: "purple", size: "xs", children: tag }, tag)))] }), hit.snippet && (_jsx("p", { className: "mt-3 line-clamp-2 text-sm text-gray-500 dark:text-gray-400", children: hit.snippet }))] }), _jsx("div", { className: "ml-3 shrink-0 pt-1", children: expandedDocId === hit.document_id ? (_jsx(HiChevronUp, { className: "h-5 w-5 text-gray-400" })) : (_jsx(HiChevronDown, { className: "h-5 w-5 text-gray-400" })) })] }) }), expandedDocId === hit.document_id && (_jsx("div", { className: "border-t border-gray-200 pt-4 dark:border-gray-700", children: sectionLoading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [_jsx(Spinner, { size: "sm" }), "Loading full content..."] })) : sectionContent ? (_jsxs("div", { children: [_jsx("div", { className: "prose prose-sm prose-gray max-h-96 max-w-none overflow-auto rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: {
|
|
155
|
-
h1: ({ children }) => (_jsx("h1", { className: "text-gray-900 dark:text-white", children: children })),
|
|
156
|
-
h2: ({ children }) => (_jsx("h2", { className: "text-gray-900 dark:text-white", children: children })),
|
|
157
|
-
h3: ({ children }) => (_jsx("h3", { className: "text-gray-900 dark:text-white", children: children })),
|
|
158
|
-
h4: ({ children }) => (_jsx("h4", { className: "text-gray-900 dark:text-white", children: children })),
|
|
159
|
-
p: ({ children }) => (_jsx("p", { className: "text-gray-700 dark:text-gray-200", children: children })),
|
|
160
|
-
li: ({ children }) => (_jsx("li", { className: "text-gray-700 dark:text-gray-200", children: children })),
|
|
161
|
-
strong: ({ children }) => (_jsx("strong", { className: "text-gray-900 dark:text-white", children: children })),
|
|
162
|
-
table: ({ children }) => (_jsx("div", { className: "overflow-x-auto", children: _jsx("table", { className: "min-w-full border-collapse text-sm", children: children }) })),
|
|
163
|
-
thead: ({ children }) => (_jsx("thead", { className: "border-b-2 border-gray-300 dark:border-gray-600", children: children })),
|
|
164
|
-
th: ({ children }) => (_jsx("th", { className: "px-3 py-2 text-left font-semibold text-gray-900 dark:text-white", children: children })),
|
|
165
|
-
td: ({ children }) => (_jsx("td", { className: "border-t border-gray-200 px-3 py-2 text-gray-700 dark:border-gray-700 dark:text-gray-200", children: children })),
|
|
166
|
-
}, children: sectionContent.content }) }), sectionContent.content_length && (_jsxs("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: [sectionContent.content_length.toLocaleString(), ' ', "characters"] }))] })) : (_jsx("p", { className: "text-sm text-gray-500", children: "Could not load section content." })) }))] }, hit.document_id));
|
|
167
|
-
}), total > PAGE_SIZE && (_jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx(Button, { color: "gray", size: "sm", disabled: offset === 0 || loading, onClick: () => handleSearch(Math.max(0, offset - PAGE_SIZE)), children: "Previous" }), _jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: ["Page ", Math.floor(offset / PAGE_SIZE) + 1, " of", ' ', Math.ceil(total / PAGE_SIZE)] }), _jsx(Button, { color: "gray", size: "sm", disabled: offset + PAGE_SIZE >= total || loading, onClick: () => handleSearch(offset + PAGE_SIZE), children: "Next" })] }))] })), !loading && searchedQuery && results.length === 0 && (_jsxs("div", { className: "py-8 text-center", children: [_jsxs("p", { className: "text-gray-500 dark:text-gray-400", children: ["No results found for \"", searchedQuery, "\"."] }), _jsx("p", { className: "mt-1 text-sm text-gray-400 dark:text-gray-500", children: "Try a different query or adjust your filters." })] }))] }));
|
|
150
|
+
return (_jsxs(PageLayout, { children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "from-primary-500 to-secondary-600 rounded-lg bg-gradient-to-br p-3", children: _jsx(HiSearch, { className: "h-8 w-8 text-white" }) }), _jsxs("div", { children: [_jsx("h1", { className: "font-heading text-3xl font-bold text-gray-900 dark:text-white", children: config.title }), _jsxs("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [config.description, docCount !== null && docCount > 0 && (_jsxs("span", { className: "ml-2 text-gray-400 dark:text-gray-500", children: ["(", docCount, " document", docCount !== 1 ? 's' : '', " indexed)"] }))] })] })] }), _jsx(Card, { children: _jsxs("div", { className: "space-y-4", children: [_jsx(SearchBar, { query: query, onQueryChange: setQuery, onSearch: () => handleSearch(0), loading: loading, placeholder: config.placeholder }), _jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [hasFilters && (_jsxs("button", { type: "button", onClick: () => setShowFilters(!showFilters), className: "flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", children: ["Filters", showFilters ? (_jsx(HiChevronUp, { className: "h-4 w-4" })) : (_jsx(HiChevronDown, { className: "h-4 w-4" }))] })), filters.semantic && (_jsx(ToggleSwitch, { checked: semantic, onChange: setSemantic, label: "Semantic search" }))] }), showFilters && hasFilters && (_jsxs("div", { className: "grid grid-cols-1 gap-3 border-t border-gray-200 pt-4 sm:grid-cols-2 lg:grid-cols-4 dark:border-gray-700", children: [filters.sourceType && (_jsxs("div", { children: [_jsx("label", { htmlFor: "search-source-type", className: "mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400", children: "Source Type" }), _jsxs(Select, { id: "search-source-type", value: sourceType, onChange: (e) => setSourceType(e.target.value), children: [_jsx("option", { value: "", children: "All types" }), _jsx("option", { value: "uploaded_doc", children: "Uploaded Documents" }), _jsx("option", { value: "memory", children: "AI Memories" }), _jsx("option", { value: "narrative_section", children: "Narrative Sections" }), _jsx("option", { value: "xbrl_textblock", children: "XBRL Text Blocks" }), _jsx("option", { value: "ixbrl_disclosure", children: "iXBRL Disclosures" })] })] })), filters.entity && (_jsxs("div", { children: [_jsx("label", { htmlFor: "search-entity", className: "mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400", children: "Entity / Ticker" }), _jsx(TextInput, { id: "search-entity", placeholder: "e.g. NVDA", value: entity, onChange: (e) => setEntity(e.target.value) })] })), filters.formType && (_jsxs("div", { children: [_jsx("label", { htmlFor: "search-form-type", className: "mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400", children: "Form Type" }), _jsxs(Select, { id: "search-form-type", value: formType, onChange: (e) => setFormType(e.target.value), children: [_jsx("option", { value: "", children: "All forms" }), _jsx("option", { value: "10-K", children: "10-K (Annual)" }), _jsx("option", { value: "10-Q", children: "10-Q (Quarterly)" }), _jsx("option", { value: "8-K", children: "8-K (Current)" }), _jsx("option", { value: "20-F", children: "20-F (Foreign Annual)" })] })] })), filters.fiscalYear && (_jsxs("div", { children: [_jsx("label", { htmlFor: "search-fiscal-year", className: "mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400", children: "Fiscal Year" }), _jsx(TextInput, { id: "search-fiscal-year", type: "number", placeholder: "e.g. 2024", min: 1900, max: 2100, value: fiscalYear, onChange: (e) => setFiscalYear(e.target.value) })] }))] }))] }) }), error && (_jsx(Alert, { color: "failure", children: _jsx("span", { children: error }) })), results.length > 0 && (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsx(SearchResultsMeta, { total: total, count: results.length, offset: offset, query: searchedQuery }) }), results.map((hit) => (_jsx(SearchHitCard, { hit: hit, expanded: expandedDocId === hit.document_id, onClick: () => handleExpand(hit.document_id), children: sectionLoading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [_jsx(Spinner, { size: "sm" }), "Loading full content..."] })) : sectionContent ? (_jsxs("div", { children: [_jsx("div", { className: "max-h-96 overflow-auto rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800", children: _jsx(MarkdownProse, { size: "sm", children: sectionContent.content }) }), sectionContent.content_length && (_jsxs("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: [sectionContent.content_length.toLocaleString(), ' ', "characters"] }))] })) : (_jsx("p", { className: "text-sm text-gray-500", children: "Could not load section content." })) }, hit.document_id))), _jsx(SearchPagination, { total: total, offset: offset, pageSize: PAGE_SIZE, loading: loading, onPageChange: handleSearch })] })), !loading && searchedQuery && results.length === 0 && (_jsxs("div", { className: "py-8 text-center", children: [_jsxs("p", { className: "text-gray-500 dark:text-gray-400", children: ["No results found for \"", searchedQuery, "\"."] }), _jsx("p", { className: "mt-1 text-sm text-gray-400 dark:text-gray-500", children: "Try a different query or adjust your filters." })] }))] }));
|
|
168
151
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SearchHit } from '@robosystems/client';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
export interface SearchHitCardProps {
|
|
4
|
+
hit: SearchHit;
|
|
5
|
+
/** Card activation — Search toggles expansion; Recall opens the memory. */
|
|
6
|
+
onClick?: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Expansion state. Leave undefined for surfaces without an expand
|
|
9
|
+
* affordance (no chevron is rendered).
|
|
10
|
+
*/
|
|
11
|
+
expanded?: boolean;
|
|
12
|
+
/** Title row (document/section). Recall hits are title-less. */
|
|
13
|
+
showTitle?: boolean;
|
|
14
|
+
showSourceType?: boolean;
|
|
15
|
+
tagBadgeColor?: string;
|
|
16
|
+
/** Expanded region, rendered under a top border when `expanded` is true. */
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The standard scored-search-result card: title row, badge row (score,
|
|
21
|
+
* source type, entity, form, fiscal year, tags), snippet, and an optional
|
|
22
|
+
* expandable region. Shared by the document Search page and the memory
|
|
23
|
+
* Recall panel.
|
|
24
|
+
*/
|
|
25
|
+
export declare function SearchHitCard({ hit, onClick, expanded, showTitle, showSourceType, tagBadgeColor, children, }: SearchHitCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, Card } from 'flowbite-react';
|
|
4
|
+
import { HiChevronDown, HiChevronUp } from 'react-icons/hi';
|
|
5
|
+
/**
|
|
6
|
+
* The standard scored-search-result card: title row, badge row (score,
|
|
7
|
+
* source type, entity, form, fiscal year, tags), snippet, and an optional
|
|
8
|
+
* expandable region. Shared by the document Search page and the memory
|
|
9
|
+
* Recall panel.
|
|
10
|
+
*/
|
|
11
|
+
export function SearchHitCard({ hit, onClick, expanded, showTitle = true, showSourceType = true, tagBadgeColor = 'purple', children, }) {
|
|
12
|
+
var _a;
|
|
13
|
+
const expandable = expanded !== undefined;
|
|
14
|
+
return (_jsxs(Card, { children: [_jsx("button", { type: "button", className: "w-full cursor-pointer text-left", onClick: onClick, "aria-expanded": expandable ? expanded : undefined, children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [showTitle && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h3", { className: "text-base font-semibold text-gray-900 dark:text-white", children: hit.document_title || hit.section_label || 'Untitled' }), hit.section_label && hit.document_title && (_jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: ["/ ", hit.section_label] }))] })), _jsxs("div", { className: `flex flex-wrap items-center gap-2 ${showTitle ? 'mt-2' : ''}`, children: [_jsx(Badge, { color: "info", size: "xs", children: hit.score.toFixed(2) }), showSourceType && (_jsx(Badge, { color: "gray", size: "xs", children: hit.source_type })), hit.entity_ticker && (_jsx(Badge, { color: "purple", size: "xs", children: hit.entity_ticker })), hit.form_type && (_jsx(Badge, { color: "warning", size: "xs", children: hit.form_type })), hit.fiscal_year && (_jsxs(Badge, { color: "gray", size: "xs", children: ["FY", hit.fiscal_year] })), (_a = hit.tags) === null || _a === void 0 ? void 0 : _a.map((tag) => (_jsx(Badge, { color: tagBadgeColor, size: "xs", children: tag }, tag)))] }), hit.snippet && (_jsx("p", { className: "mt-3 line-clamp-2 text-sm text-gray-500 dark:text-gray-400", children: hit.snippet }))] }), expandable && (_jsx("div", { className: "ml-3 shrink-0 pt-1", children: expanded ? (_jsx(HiChevronUp, { className: "h-5 w-5 text-gray-400" })) : (_jsx(HiChevronDown, { className: "h-5 w-5 text-gray-400" })) }))] }) }), expanded && children && (_jsx("div", { className: "border-t border-gray-200 pt-4 dark:border-gray-700", children: children }))] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SearchPaginationProps {
|
|
2
|
+
total: number;
|
|
3
|
+
offset: number;
|
|
4
|
+
pageSize: number;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
/** Receives the new offset — the caller re-runs its search. */
|
|
7
|
+
onPageChange: (newOffset: number) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Previous / "Page X of Y" / Next pagination for offset-based search
|
|
11
|
+
* results. Renders nothing when everything fits on one page.
|
|
12
|
+
*/
|
|
13
|
+
export declare function SearchPagination({ total, offset, pageSize, loading, onPageChange, }: SearchPaginationProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from 'flowbite-react';
|
|
4
|
+
/**
|
|
5
|
+
* Previous / "Page X of Y" / Next pagination for offset-based search
|
|
6
|
+
* results. Renders nothing when everything fits on one page.
|
|
7
|
+
*/
|
|
8
|
+
export function SearchPagination({ total, offset, pageSize, loading = false, onPageChange, }) {
|
|
9
|
+
if (total <= pageSize)
|
|
10
|
+
return null;
|
|
11
|
+
return (_jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx(Button, { color: "gray", size: "sm", disabled: offset === 0 || loading, onClick: () => onPageChange(Math.max(0, offset - pageSize)), children: "Previous" }), _jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: ["Page ", Math.floor(offset / pageSize) + 1, " of", ' ', Math.ceil(total / pageSize)] }), _jsx(Button, { color: "gray", size: "sm", disabled: offset + pageSize >= total || loading, onClick: () => onPageChange(offset + pageSize), children: "Next" })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface SearchResultsMetaProps {
|
|
3
|
+
total: number;
|
|
4
|
+
/** Hits rendered on the current page. */
|
|
5
|
+
count: number;
|
|
6
|
+
offset?: number;
|
|
7
|
+
query: string;
|
|
8
|
+
/** Overrides the default copy (Recall supplies its own phrasing). */
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The results-summary line above a search hit list. Default copy is the
|
|
13
|
+
* Search page's "Showing X–Y of N results for "q""; pass children for
|
|
14
|
+
* surface-specific phrasing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function SearchResultsMeta({ total, count, offset, query, children, }: SearchResultsMetaProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The results-summary line above a search hit list. Default copy is the
|
|
4
|
+
* Search page's "Showing X–Y of N results for "q""; pass children for
|
|
5
|
+
* surface-specific phrasing.
|
|
6
|
+
*/
|
|
7
|
+
export function SearchResultsMeta({ total, count, offset = 0, query, children, }) {
|
|
8
|
+
return (_jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: children !== null && children !== void 0 ? children : (_jsxs(_Fragment, { children: ["Showing ", offset + 1, "\u2013", Math.min(offset + count, total), " of", ' ', total, " results for \"", query, "\""] })) }));
|
|
9
|
+
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
export { SearchBar, type SearchBarProps } from './SearchBar';
|
|
1
2
|
export { SearchContent } from './SearchContent';
|
|
3
|
+
export { SearchHitCard, type SearchHitCardProps } from './SearchHitCard';
|
|
4
|
+
export { SearchPagination, type SearchPaginationProps, } from './SearchPagination';
|
|
5
|
+
export { SearchResultsMeta, type SearchResultsMetaProps, } from './SearchResultsMeta';
|
|
2
6
|
export type { SearchConfig, SearchFilterConfig } from './types';
|
package/index.d.ts
CHANGED
|
@@ -12,9 +12,10 @@ 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 { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchContent, useIsRepository, type ActiveSubscriptionsProps, type BrowseRepositoriesProps, type EntityGroup, type EntityLike, type EntityRecord, type EntitySelectorProps, type GraphSelectorProps, type GraphWithEntities, type SearchConfig, type SearchFilterConfig, type SelectableEntity, } from './components';
|
|
15
|
+
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
16
|
export { composeFilters, excludeRepositories, excludeSubgraphs, GraphFilters, hasSchemaExtension, onlyRepositories, onlyUserGraphs, } from './components/graph-filters';
|
|
17
|
-
export { ApiKeyDisplay, ApiKeysCard, ApiKeyTable, BrandSpinner, categorizeError, ChatHeader, ChatInputArea, ChatMessage, ConfirmModal, CoreNavbar, CoreSidebar, CreateApiKeyModal, DeepResearchToggle, EmptyState, ErrorType, GeneralInformationCard, generateMessageId, getErrorMessage, LandingFooter, LoadingState, PageContainer, PageHeader, PasswordInformationCard, PasswordRequirements, SecureApiKeyField, SettingsCard, SettingsContainer, SettingsFormField, SettingsPageHeader, Spinner, StatCard, StatusAlert, SupportModal, ThemeToggle, } from './ui-components';
|
|
17
|
+
export { ApiKeyDisplay, ApiKeysCard, ApiKeyTable, 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';
|
|
18
|
+
export type { CategoryInputProps, MarkdownProseProps, TagInputProps, } from './ui-components';
|
|
18
19
|
export type { AgentType, FooterLink, LandingFooterProps, Message, PageHeaderProps, SidebarItemData, SupportMetadata, } from './ui-components';
|
|
19
20
|
export * from './library';
|
|
20
21
|
export * as SDK from '@robosystems/client';
|
package/index.js
CHANGED
|
@@ -31,13 +31,13 @@ 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 { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchContent, useIsRepository, } from './components';
|
|
34
|
+
export { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, useIsRepository, } from './components';
|
|
35
35
|
// Export graph filters
|
|
36
36
|
export { composeFilters, excludeRepositories, excludeSubgraphs, GraphFilters, hasSchemaExtension, onlyRepositories, onlyUserGraphs, } from './components/graph-filters';
|
|
37
37
|
// Export app-components
|
|
38
|
-
export { ApiKeyDisplay, ApiKeysCard, ApiKeyTable, BrandSpinner, categorizeError, ChatHeader, ChatInputArea,
|
|
38
|
+
export { ApiKeyDisplay, ApiKeysCard, ApiKeyTable, BrandSpinner, categorizeError, CategoryInput, ChatHeader, ChatInputArea,
|
|
39
39
|
// Chat components
|
|
40
|
-
ChatMessage, ConfirmModal, CoreNavbar, CoreSidebar, CreateApiKeyModal, DeepResearchToggle, EmptyState, ErrorType, GeneralInformationCard, generateMessageId, getErrorMessage, LandingFooter, LoadingState, PageContainer, PageHeader, PasswordInformationCard, PasswordRequirements, SecureApiKeyField, SettingsCard, SettingsContainer, SettingsFormField, SettingsPageHeader, Spinner, StatCard, StatusAlert, SupportModal, ThemeToggle, } from './ui-components';
|
|
40
|
+
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';
|
|
41
41
|
// Export library components, types, and helpers
|
|
42
42
|
export * from './library';
|
|
43
43
|
// Export SDK with namespace to avoid conflicts
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
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;
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
package/research/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
|
|
2
|
+
export { CoverageBrowser, type CoverageBrowserProps } from './CoverageBrowser';
|
|
2
3
|
export { CoverageCard } from './CoverageCard';
|
|
3
4
|
export { CoverageGrid } from './CoverageGrid';
|
|
4
5
|
export { CoverageHistory } from './CoverageHistory';
|
package/research/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// roboinvestor-app (logged-in). Data layer reads the S3 catalog produced by
|
|
3
3
|
// robosystems-content-machine; components render the coverage + continuing-coverage thread.
|
|
4
4
|
export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
|
|
5
|
+
export { CoverageBrowser } from './CoverageBrowser';
|
|
5
6
|
export { CoverageCard } from './CoverageCard';
|
|
6
7
|
export { CoverageGrid } from './CoverageGrid';
|
|
7
8
|
export { CoverageHistory } from './CoverageHistory';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface MarkdownProseProps {
|
|
2
|
+
/** Markdown source. */
|
|
3
|
+
children: string;
|
|
4
|
+
/** Typography scale. */
|
|
5
|
+
size?: 'sm' | 'base';
|
|
6
|
+
/** Merged over the prose classes — scroll containers, padding, etc. */
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The shared GitHub-flavored-markdown prose renderer (documents, memories,
|
|
11
|
+
* search results). Theme-aware via `dark:prose-dark` — the consuming apps
|
|
12
|
+
* define the `typography.dark` variant in their Tailwind configs; do NOT
|
|
13
|
+
* switch to `dark:prose-invert`, which doesn't resolve under their
|
|
14
|
+
* Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
|
|
15
|
+
* Works in server and client components alike.
|
|
16
|
+
*/
|
|
17
|
+
export declare function MarkdownProse({ children, size, className, }: MarkdownProseProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
/**
|
|
6
|
+
* The shared GitHub-flavored-markdown prose renderer (documents, memories,
|
|
7
|
+
* search results). Theme-aware via `dark:prose-dark` — the consuming apps
|
|
8
|
+
* define the `typography.dark` variant in their Tailwind configs; do NOT
|
|
9
|
+
* switch to `dark:prose-invert`, which doesn't resolve under their
|
|
10
|
+
* Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
|
|
11
|
+
* Works in server and client components alike.
|
|
12
|
+
*/
|
|
13
|
+
export function MarkdownProse({ children, size = 'base', className, }) {
|
|
14
|
+
return (_jsx("div", { className: twMerge('prose dark:prose-dark max-w-none', size === 'sm' ? 'prose-sm' : 'prose-base', className), children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: {
|
|
15
|
+
table: ({ children: tableChildren }) => (_jsx("div", { className: "overflow-x-auto", children: _jsx("table", { children: tableChildren }) })),
|
|
16
|
+
}, children: children }) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface CategoryInputProps {
|
|
2
|
+
/** Controlled value — always freeform; suggestions are conveniences. */
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Suggestion pool (presets and/or observed values). All are shown on
|
|
7
|
+
* focus; typing filters them case-insensitively.
|
|
8
|
+
*/
|
|
9
|
+
suggestions?: string[];
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
sizing?: 'sm' | 'md';
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Width control, e.g. 'w-40'. */
|
|
15
|
+
className?: string;
|
|
16
|
+
'aria-label'?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Freeform combobox for single-value categorization (document folder,
|
|
20
|
+
* memory type). Suggestions open on focus and filter as the user types;
|
|
21
|
+
* ArrowUp/ArrowDown + Enter or click selects one, Escape closes, and any
|
|
22
|
+
* typed value is accepted as-is.
|
|
23
|
+
*/
|
|
24
|
+
export declare function CategoryInput({ value, onChange, suggestions, placeholder, id, sizing, disabled, className, 'aria-label': ariaLabel, }: CategoryInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { TextInput } from 'flowbite-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
6
|
+
/**
|
|
7
|
+
* Freeform combobox for single-value categorization (document folder,
|
|
8
|
+
* memory type). Suggestions open on focus and filter as the user types;
|
|
9
|
+
* ArrowUp/ArrowDown + Enter or click selects one, Escape closes, and any
|
|
10
|
+
* typed value is accepted as-is.
|
|
11
|
+
*/
|
|
12
|
+
export function CategoryInput({ value, onChange, suggestions, placeholder, id, sizing = 'sm', disabled = false, className, 'aria-label': ariaLabel, }) {
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
15
|
+
const query = value.trim().toLowerCase();
|
|
16
|
+
const visibleSuggestions = (suggestions !== null && suggestions !== void 0 ? suggestions : [])
|
|
17
|
+
.filter((s) => !query || s.toLowerCase().includes(query))
|
|
18
|
+
.slice(0, MAX_VISIBLE_SUGGESTIONS);
|
|
19
|
+
const showSuggestions = open && !disabled && visibleSuggestions.length > 0;
|
|
20
|
+
const select = (suggestion) => {
|
|
21
|
+
onChange(suggestion);
|
|
22
|
+
setOpen(false);
|
|
23
|
+
setActiveIndex(-1);
|
|
24
|
+
};
|
|
25
|
+
const handleKeyDown = (e) => {
|
|
26
|
+
if (!showSuggestions) {
|
|
27
|
+
if (e.key === 'Enter')
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (e.key === 'ArrowDown') {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
setActiveIndex((i) => (i + 1) % visibleSuggestions.length);
|
|
34
|
+
}
|
|
35
|
+
else if (e.key === 'ArrowUp') {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
setActiveIndex((i) => (i <= 0 ? visibleSuggestions.length - 1 : i - 1));
|
|
38
|
+
}
|
|
39
|
+
else if (e.key === 'Enter') {
|
|
40
|
+
// Never submits a surrounding form; with no active item it just closes.
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
if (activeIndex >= 0 && activeIndex < visibleSuggestions.length) {
|
|
43
|
+
select(visibleSuggestions[activeIndex]);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
setOpen(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (e.key === 'Escape') {
|
|
50
|
+
setOpen(false);
|
|
51
|
+
setActiveIndex(-1);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return (_jsxs("div", { className: `relative ${className !== null && className !== void 0 ? className : ''}`, children: [_jsx(TextInput, { id: id, sizing: sizing, value: value, disabled: disabled, placeholder: placeholder, onChange: (e) => {
|
|
55
|
+
onChange(e.target.value);
|
|
56
|
+
setOpen(true);
|
|
57
|
+
setActiveIndex(-1);
|
|
58
|
+
}, onFocus: () => setOpen(true), onBlur: () => {
|
|
59
|
+
setOpen(false);
|
|
60
|
+
setActiveIndex(-1);
|
|
61
|
+
}, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": showSuggestions, "aria-label": ariaLabel, autoComplete: "off" }), showSuggestions && (_jsx("ul", { role: "listbox", "aria-label": "Suggestions", className: "absolute z-10 mt-1 min-w-full rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800", children: visibleSuggestions.map((suggestion, index) => (_jsx("li", { role: "option", "aria-selected": index === activeIndex, children: _jsx("button", { type: "button", className: `w-full px-3 py-1.5 text-left text-sm ${index === activeIndex
|
|
62
|
+
? 'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-white'
|
|
63
|
+
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700'}`,
|
|
64
|
+
// onMouseDown so selection wins over the input's blur.
|
|
65
|
+
onMouseDown: (e) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
select(suggestion);
|
|
68
|
+
}, children: suggestion }) }, suggestion))) }))] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface TagInputProps {
|
|
2
|
+
/** Controlled tag list. */
|
|
3
|
+
tags: string[];
|
|
4
|
+
onChange: (tags: string[]) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Suggestion pool (e.g. tags observed elsewhere in the list). Filtered
|
|
7
|
+
* case-insensitively against the draft input, minus already-added tags.
|
|
8
|
+
*/
|
|
9
|
+
suggestions?: string[];
|
|
10
|
+
/** flowbite Badge color for the tag chips. */
|
|
11
|
+
badgeColor?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Class for the inline draft input — callers control its width. */
|
|
16
|
+
inputClassName?: string;
|
|
17
|
+
'aria-label'?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Chips-style tag editor. Enter or comma commits the draft (trimmed,
|
|
21
|
+
* lowercased, deduped); Backspace on an empty draft removes the last tag.
|
|
22
|
+
* Pasted comma-separated text is split into individual tags. An optional
|
|
23
|
+
* suggestion listbox opens while the input is focused.
|
|
24
|
+
*/
|
|
25
|
+
export declare function TagInput({ tags, onChange, suggestions, badgeColor, placeholder, id, disabled, inputClassName, 'aria-label': ariaLabel, }: TagInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, TextInput } from 'flowbite-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { HiX } from 'react-icons/hi';
|
|
6
|
+
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
7
|
+
/**
|
|
8
|
+
* Chips-style tag editor. Enter or comma commits the draft (trimmed,
|
|
9
|
+
* lowercased, deduped); Backspace on an empty draft removes the last tag.
|
|
10
|
+
* Pasted comma-separated text is split into individual tags. An optional
|
|
11
|
+
* suggestion listbox opens while the input is focused.
|
|
12
|
+
*/
|
|
13
|
+
export function TagInput({ tags, onChange, suggestions, badgeColor = 'purple', placeholder = 'Add tag, press Enter', id, disabled = false, inputClassName = 'w-44', 'aria-label': ariaLabel, }) {
|
|
14
|
+
const [draft, setDraft] = useState('');
|
|
15
|
+
const [focused, setFocused] = useState(false);
|
|
16
|
+
const normalize = (value) => value.trim().toLowerCase();
|
|
17
|
+
const commitValues = (values) => {
|
|
18
|
+
const next = [...tags];
|
|
19
|
+
for (const value of values) {
|
|
20
|
+
const normalized = normalize(value);
|
|
21
|
+
if (normalized && !next.includes(normalized)) {
|
|
22
|
+
next.push(normalized);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (next.length !== tags.length) {
|
|
26
|
+
onChange(next);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const handleDraftChange = (value) => {
|
|
30
|
+
var _a;
|
|
31
|
+
// Pasted "a, b, c" — commit every complete segment, keep the tail as draft.
|
|
32
|
+
if (value.includes(',')) {
|
|
33
|
+
const segments = value.split(',');
|
|
34
|
+
const tail = (_a = segments.pop()) !== null && _a !== void 0 ? _a : '';
|
|
35
|
+
commitValues(segments);
|
|
36
|
+
setDraft(tail);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
setDraft(value);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const handleKeyDown = (e) => {
|
|
43
|
+
if (e.key === 'Enter' || e.key === ',') {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
commitValues([draft]);
|
|
46
|
+
setDraft('');
|
|
47
|
+
}
|
|
48
|
+
else if (e.key === 'Backspace' && draft === '' && tags.length > 0) {
|
|
49
|
+
onChange(tags.slice(0, -1));
|
|
50
|
+
}
|
|
51
|
+
else if (e.key === 'Escape') {
|
|
52
|
+
setFocused(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const draftNormalized = normalize(draft);
|
|
56
|
+
const visibleSuggestions = (suggestions !== null && suggestions !== void 0 ? suggestions : [])
|
|
57
|
+
.filter((s) => !tags.includes(normalize(s)) &&
|
|
58
|
+
(!draftNormalized || s.toLowerCase().includes(draftNormalized)))
|
|
59
|
+
.slice(0, MAX_VISIBLE_SUGGESTIONS);
|
|
60
|
+
const showSuggestions = focused && !disabled && visibleSuggestions.length > 0;
|
|
61
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [tags.map((tag) => (_jsx(Badge, { color: badgeColor, size: "sm", children: _jsxs("span", { className: "flex items-center gap-1", children: [tag, _jsx("button", { type: "button", "aria-label": `Remove tag ${tag}`, disabled: disabled, onClick: () => onChange(tags.filter((t) => t !== tag)), children: _jsx(HiX, { className: "h-3 w-3" }) })] }) }, tag))), _jsxs("div", { className: "relative", children: [_jsx(TextInput, { id: id, sizing: "sm", value: draft, disabled: disabled, onChange: (e) => handleDraftChange(e.target.value), onKeyDown: handleKeyDown, onFocus: () => setFocused(true), onBlur: () => setFocused(false), placeholder: placeholder, className: inputClassName, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : 'Add tag', role: "combobox", "aria-expanded": showSuggestions, autoComplete: "off" }), showSuggestions && (_jsx("ul", { role: "listbox", "aria-label": "Tag suggestions", className: "absolute z-10 mt-1 min-w-full rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800", children: visibleSuggestions.map((suggestion) => (_jsx("li", { role: "option", "aria-selected": false, children: _jsx("button", { type: "button", className: "w-full px-3 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700",
|
|
62
|
+
// onMouseDown so selection wins over the input's blur.
|
|
63
|
+
onMouseDown: (e) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
commitValues([suggestion]);
|
|
66
|
+
setDraft('');
|
|
67
|
+
}, children: suggestion }) }, suggestion))) }))] })] }));
|
|
68
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export { CategoryInput, type CategoryInputProps } from './CategoryInput';
|
|
1
2
|
export { PasswordRequirements } from './PasswordRequirements';
|
|
2
3
|
export { SettingsCard } from './SettingsCard';
|
|
3
4
|
export { SettingsFormField } from './SettingsFormField';
|
|
4
5
|
export { StatusAlert } from './StatusAlert';
|
|
6
|
+
export { TagInput, type TagInputProps } from './TagInput';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export { CategoryInput } from './CategoryInput';
|
|
1
2
|
export { PasswordRequirements } from './PasswordRequirements';
|
|
2
3
|
export { SettingsCard } from './SettingsCard';
|
|
3
4
|
export { SettingsFormField } from './SettingsFormField';
|
|
4
5
|
export { StatusAlert } from './StatusAlert';
|
|
6
|
+
export { TagInput } from './TagInput';
|
package/ui-components/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { ConfirmModal } from './ConfirmModal';
|
|
|
6
6
|
export { EmptyState } from './EmptyState';
|
|
7
7
|
export { LoadingState } from './LoadingState';
|
|
8
8
|
export { AnimatedLogo, LogoBadge } from './Logo';
|
|
9
|
+
export { MarkdownProse, type MarkdownProseProps } from './MarkdownProse';
|
|
9
10
|
export { BrandSpinner, Spinner } from './Spinner';
|
|
10
11
|
export { StatCard } from './StatCard';
|
|
11
12
|
export * from './chat';
|
package/ui-components/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { ConfirmModal } from './ConfirmModal';
|
|
|
8
8
|
export { EmptyState } from './EmptyState';
|
|
9
9
|
export { LoadingState } from './LoadingState';
|
|
10
10
|
export { AnimatedLogo, LogoBadge } from './Logo';
|
|
11
|
+
export { MarkdownProse } from './MarkdownProse';
|
|
11
12
|
export { BrandSpinner, Spinner } from './Spinner';
|
|
12
13
|
export { StatCard } from './StatCard';
|
|
13
14
|
// Chat Components
|