@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.
Files changed (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /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;