@agent-relay/dashboard 2.0.81 → 2.0.82
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/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchResults Component
|
|
3
|
+
*
|
|
4
|
+
* Displays search results with highlighted snippets and pagination.
|
|
5
|
+
* Supports clicking to navigate to specific messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useCallback } from 'react';
|
|
9
|
+
import type { SearchResultsProps, SearchResult } from './types';
|
|
10
|
+
|
|
11
|
+
export function SearchResults({
|
|
12
|
+
results,
|
|
13
|
+
total,
|
|
14
|
+
query,
|
|
15
|
+
isLoading = false,
|
|
16
|
+
hasMore = false,
|
|
17
|
+
error,
|
|
18
|
+
onLoadMore,
|
|
19
|
+
onResultClick,
|
|
20
|
+
}: SearchResultsProps) {
|
|
21
|
+
// Format relative time
|
|
22
|
+
const formatTime = useCallback((timestamp: string) => {
|
|
23
|
+
const date = new Date(timestamp);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const diffMs = now.getTime() - date.getTime();
|
|
26
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
27
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
28
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
29
|
+
|
|
30
|
+
if (diffMins < 1) return 'just now';
|
|
31
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
32
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
33
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
34
|
+
return date.toLocaleDateString();
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// Error state
|
|
38
|
+
if (error) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="p-6 text-center">
|
|
41
|
+
<ErrorIcon className="w-8 h-8 text-red-400 mx-auto mb-2" />
|
|
42
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Loading state (initial)
|
|
48
|
+
if (isLoading && results.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="p-6 text-center">
|
|
51
|
+
<LoadingSpinner className="w-6 h-6 text-accent-cyan mx-auto mb-2" />
|
|
52
|
+
<p className="text-sm text-text-muted">Searching...</p>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Empty state
|
|
58
|
+
if (!isLoading && results.length === 0 && query) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="p-6 text-center">
|
|
61
|
+
<SearchIcon className="w-8 h-8 text-text-muted mx-auto mb-2" />
|
|
62
|
+
<p className="text-sm text-text-muted">No results found for "{query}"</p>
|
|
63
|
+
<p className="text-xs text-text-muted mt-1">Try different keywords</p>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// No query state
|
|
69
|
+
if (results.length === 0 && !query) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="p-6 text-center">
|
|
72
|
+
<SearchIcon className="w-8 h-8 text-text-muted mx-auto mb-2" />
|
|
73
|
+
<p className="text-sm text-text-muted">Enter a search term to find messages</p>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex flex-col">
|
|
80
|
+
{/* Results count */}
|
|
81
|
+
<div className="px-4 py-2 border-b border-border-subtle bg-bg-secondary/50">
|
|
82
|
+
<p className="text-xs text-text-muted">
|
|
83
|
+
{total === 1 ? '1 result' : `${total} results`} for "{query}"
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Results list */}
|
|
88
|
+
<div className="flex-1 overflow-y-auto">
|
|
89
|
+
{results.map((result) => (
|
|
90
|
+
<SearchResultItem
|
|
91
|
+
key={result.id}
|
|
92
|
+
result={result}
|
|
93
|
+
query={query}
|
|
94
|
+
formatTime={formatTime}
|
|
95
|
+
onClick={onResultClick}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
|
|
99
|
+
{/* Load more button */}
|
|
100
|
+
{hasMore && (
|
|
101
|
+
<div className="p-4 text-center border-t border-border-subtle">
|
|
102
|
+
<button
|
|
103
|
+
onClick={onLoadMore}
|
|
104
|
+
disabled={isLoading}
|
|
105
|
+
className="px-4 py-2 text-sm text-accent-cyan hover:text-accent-cyan/80 disabled:opacity-50 transition-colors"
|
|
106
|
+
>
|
|
107
|
+
{isLoading ? (
|
|
108
|
+
<span className="flex items-center gap-2 justify-center">
|
|
109
|
+
<LoadingSpinner className="w-4 h-4" />
|
|
110
|
+
Loading more...
|
|
111
|
+
</span>
|
|
112
|
+
) : (
|
|
113
|
+
`Load more results`
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Search Result Item
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
interface SearchResultItemProps {
|
|
128
|
+
result: SearchResult;
|
|
129
|
+
query: string;
|
|
130
|
+
formatTime: (timestamp: string) => string;
|
|
131
|
+
onClick?: (result: SearchResult) => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function SearchResultItem({ result, query, formatTime, onClick }: SearchResultItemProps) {
|
|
135
|
+
const handleClick = useCallback(() => {
|
|
136
|
+
onClick?.(result);
|
|
137
|
+
}, [onClick, result]);
|
|
138
|
+
|
|
139
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
140
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
onClick?.(result);
|
|
143
|
+
}
|
|
144
|
+
}, [onClick, result]);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div
|
|
148
|
+
onClick={handleClick}
|
|
149
|
+
onKeyDown={handleKeyDown}
|
|
150
|
+
role="button"
|
|
151
|
+
tabIndex={0}
|
|
152
|
+
className="p-4 border-b border-border-subtle hover:bg-bg-hover cursor-pointer transition-colors focus:outline-none focus:bg-bg-hover"
|
|
153
|
+
>
|
|
154
|
+
{/* Header: channel + timestamp */}
|
|
155
|
+
<div className="flex items-center justify-between mb-1">
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<span className="text-xs font-medium text-accent-cyan">
|
|
158
|
+
#{result.channelName}
|
|
159
|
+
</span>
|
|
160
|
+
<span className="text-xs text-text-muted">•</span>
|
|
161
|
+
<span className="text-xs text-text-muted">
|
|
162
|
+
{result.from}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
<span className="text-xs text-text-muted">
|
|
166
|
+
{formatTime(result.timestamp)}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Highlighted snippet */}
|
|
171
|
+
<div className="text-sm text-text-secondary">
|
|
172
|
+
<HighlightedSnippet text={result.snippet} query={query} />
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Highlighted Snippet Component
|
|
180
|
+
// =============================================================================
|
|
181
|
+
|
|
182
|
+
interface HighlightedSnippetProps {
|
|
183
|
+
text: string;
|
|
184
|
+
query: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function HighlightedSnippet({ text, query }: HighlightedSnippetProps) {
|
|
188
|
+
const parts = useMemo(() => {
|
|
189
|
+
if (!query.trim()) {
|
|
190
|
+
return [{ text, highlight: false }];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if text already contains <b> tags from backend highlighting
|
|
194
|
+
if (text.includes('<b>') && text.includes('</b>')) {
|
|
195
|
+
// Parse backend-highlighted text
|
|
196
|
+
const segments: { text: string; highlight: boolean }[] = [];
|
|
197
|
+
let remaining = text;
|
|
198
|
+
|
|
199
|
+
while (remaining.length > 0) {
|
|
200
|
+
const startTag = remaining.indexOf('<b>');
|
|
201
|
+
|
|
202
|
+
if (startTag === -1) {
|
|
203
|
+
// No more highlights
|
|
204
|
+
segments.push({ text: remaining, highlight: false });
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Add text before highlight
|
|
209
|
+
if (startTag > 0) {
|
|
210
|
+
segments.push({ text: remaining.slice(0, startTag), highlight: false });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Find end tag
|
|
214
|
+
const endTag = remaining.indexOf('</b>', startTag);
|
|
215
|
+
if (endTag === -1) {
|
|
216
|
+
// Malformed, add rest as plain text
|
|
217
|
+
segments.push({ text: remaining.slice(startTag), highlight: false });
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Add highlighted text
|
|
222
|
+
segments.push({
|
|
223
|
+
text: remaining.slice(startTag + 3, endTag),
|
|
224
|
+
highlight: true,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
remaining = remaining.slice(endTag + 4);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return segments;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Manual highlighting based on query terms
|
|
234
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
235
|
+
const pattern = new RegExp(`(${words.map(escapeRegex).join('|')})`, 'gi');
|
|
236
|
+
const segments: { text: string; highlight: boolean }[] = [];
|
|
237
|
+
|
|
238
|
+
let lastIndex = 0;
|
|
239
|
+
let match;
|
|
240
|
+
|
|
241
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
242
|
+
// Add text before match
|
|
243
|
+
if (match.index > lastIndex) {
|
|
244
|
+
segments.push({
|
|
245
|
+
text: text.slice(lastIndex, match.index),
|
|
246
|
+
highlight: false,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Add matched text
|
|
251
|
+
segments.push({
|
|
252
|
+
text: match[0],
|
|
253
|
+
highlight: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
lastIndex = match.index + match[0].length;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add remaining text
|
|
260
|
+
if (lastIndex < text.length) {
|
|
261
|
+
segments.push({
|
|
262
|
+
text: text.slice(lastIndex),
|
|
263
|
+
highlight: false,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return segments.length > 0 ? segments : [{ text, highlight: false }];
|
|
268
|
+
}, [text, query]);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<span>
|
|
272
|
+
{parts.map((part, i) =>
|
|
273
|
+
part.highlight ? (
|
|
274
|
+
<mark
|
|
275
|
+
key={i}
|
|
276
|
+
className="bg-accent-cyan/20 text-text-primary px-0.5 rounded"
|
|
277
|
+
>
|
|
278
|
+
{part.text}
|
|
279
|
+
</mark>
|
|
280
|
+
) : (
|
|
281
|
+
<span key={i}>{part.text}</span>
|
|
282
|
+
)
|
|
283
|
+
)}
|
|
284
|
+
</span>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Escape special regex characters
|
|
289
|
+
function escapeRegex(str: string): string {
|
|
290
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// Icons
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
function SearchIcon({ className }: { className?: string }) {
|
|
298
|
+
return (
|
|
299
|
+
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
300
|
+
<circle cx="11" cy="11" r="8" />
|
|
301
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
302
|
+
</svg>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function ErrorIcon({ className }: { className?: string }) {
|
|
307
|
+
return (
|
|
308
|
+
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
309
|
+
<circle cx="12" cy="12" r="10" />
|
|
310
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
311
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
312
|
+
</svg>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function LoadingSpinner({ className }: { className?: string }) {
|
|
317
|
+
return (
|
|
318
|
+
<svg className={`${className} animate-spin`} viewBox="0 0 24 24" fill="none">
|
|
319
|
+
<circle
|
|
320
|
+
className="opacity-25"
|
|
321
|
+
cx="12"
|
|
322
|
+
cy="12"
|
|
323
|
+
r="10"
|
|
324
|
+
stroke="currentColor"
|
|
325
|
+
strokeWidth="4"
|
|
326
|
+
/>
|
|
327
|
+
<path
|
|
328
|
+
className="opacity-75"
|
|
329
|
+
fill="currentColor"
|
|
330
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
331
|
+
/>
|
|
332
|
+
</svg>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default SearchResults;
|