@peers-app/peers-ui 0.7.39 → 0.7.40

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.
@@ -98,127 +98,6 @@ const coreCommands: Command[] = [
98
98
  closeCommandPalette();
99
99
  goToTabPath('threads');
100
100
  }
101
- },
102
- {
103
- id: 'go-settings',
104
- label: 'Open Settings',
105
- description: 'Open application settings',
106
- iconClassName: 'bi-gear-fill',
107
- category: 'Navigation',
108
- action: () => {
109
- closeCommandPalette();
110
- goToTabPath('settings');
111
- }
112
- },
113
- {
114
- id: 'go-assistants',
115
- label: 'Go to Assistants',
116
- description: 'View and manage assistants',
117
- iconClassName: 'bi-person-fill-gear',
118
- category: 'Navigation',
119
- action: () => {
120
- closeCommandPalette();
121
- goToTabPath('assistants');
122
- }
123
- },
124
- {
125
- id: 'go-workflows',
126
- label: 'Go to Workflows',
127
- description: 'View and manage workflows',
128
- iconClassName: 'bi-database-fill-gear',
129
- category: 'Navigation',
130
- action: () => {
131
- closeCommandPalette();
132
- goToTabPath('workflows');
133
- }
134
- },
135
- {
136
- id: 'go-tools',
137
- label: 'Go to Tools',
138
- description: 'View and manage tools',
139
- iconClassName: 'bi-tools',
140
- category: 'Navigation',
141
- action: () => {
142
- closeCommandPalette();
143
- goToTabPath('tools');
144
- }
145
- },
146
- {
147
- id: 'go-events',
148
- label: 'Go to Events',
149
- description: 'View and manage events',
150
- iconClassName: 'bi-lightning-charge-fill',
151
- category: 'Navigation',
152
- action: () => {
153
- closeCommandPalette();
154
- goToTabPath('events');
155
- }
156
- },
157
- {
158
- id: 'go-predicates',
159
- label: 'Go to Predicates',
160
- description: 'View and manage predicates',
161
- iconClassName: 'bi-node-plus-fill',
162
- category: 'Navigation',
163
- action: () => {
164
- closeCommandPalette();
165
- goToTabPath('predicates');
166
- }
167
- },
168
- {
169
- id: 'go-peer-types',
170
- label: 'Go to Peer Types',
171
- description: 'View and manage peer types',
172
- iconClassName: 'bi-code-square',
173
- category: 'Navigation',
174
- action: () => {
175
- closeCommandPalette();
176
- goToTabPath('peer-types');
177
- }
178
- },
179
- {
180
- id: 'go-packages',
181
- label: 'Go to Packages',
182
- description: 'View and manage packages',
183
- iconClassName: 'bi-box-fill',
184
- category: 'Navigation',
185
- action: () => {
186
- closeCommandPalette();
187
- goToTabPath('packages');
188
- }
189
- },
190
- {
191
- id: 'go-variables',
192
- label: 'Go to Variables',
193
- description: 'View and manage variables',
194
- iconClassName: 'bi-braces',
195
- category: 'Navigation',
196
- action: () => {
197
- closeCommandPalette();
198
- goToTabPath('variables');
199
- }
200
- },
201
- {
202
- id: 'go-knowledge-values',
203
- label: 'Go to Knowledge Values',
204
- description: 'View and manage knowledge values',
205
- iconClassName: 'bi-journal-bookmark-fill',
206
- category: 'Navigation',
207
- action: () => {
208
- closeCommandPalette();
209
- goToTabPath('knowledge-values');
210
- }
211
- },
212
- {
213
- id: 'go-knowledge-frames',
214
- label: 'Go to Knowledge Frames',
215
- description: 'View and manage knowledge frames',
216
- iconClassName: 'bi-window-dock',
217
- category: 'Navigation',
218
- action: () => {
219
- closeCommandPalette();
220
- goToTabPath('knowledge-frames');
221
- }
222
101
  }
223
102
  ];
224
103
 
@@ -143,7 +143,7 @@ export const valueTypeMentionConfig: IMentionConfig = {
143
143
  name: item.name
144
144
  }),
145
145
  onClick(data) {
146
- rpcClientCalls.setClientPath(`value-types/${data.id}`);
146
+ rpcClientCalls.setClientPath(`peer-types/${data.id}`);
147
147
  },
148
148
  };
149
149
 
@@ -1,4 +1,4 @@
1
- import { ConsoleLogs, DataFilter, IConsoleLog } from "@peers-app/peers-sdk";
1
+ import { ConsoleLogs, DataFilter, IConsoleLog, ISubscriptionResult, newid, sleep } from "@peers-app/peers-sdk";
2
2
  import { min, sortBy, uniqBy } from 'lodash';
3
3
  import React, { Fragment, useEffect, useMemo, useState } from 'react';
4
4
  import InfiniteScroll from 'react-infinite-scroll-component';
@@ -29,14 +29,15 @@ const DEFAULT_COLUMNS: Column[] = [
29
29
  export const ConsoleLogsList = () => {
30
30
  const logs = useObservableState<IConsoleLog[]>([]);
31
31
  const [allLogsLoaded, setAllLogsLoaded] = useState(false);
32
+ const loadMoreId = useObservableState<string>(newid(), true);
32
33
  const [levelFilter, setLevelFilter] = useState<string>('all');
33
34
  const [processFilter, setProcessFilter] = useState<string>('all');
34
- const [searchText, setSearchText] = useState('');
35
+ const searchText = useObservableState<string>('');
35
36
  const [columns, setColumns] = useState<Column[]>(DEFAULT_COLUMNS);
36
37
  const [totalLogCount, setTotalLogCount] = useState<number>(0);
37
38
  const [_colorMode] = useObservable(colorMode);
38
39
  const logsEndRef = React.useRef<HTMLDivElement>(null);
39
- const containerRef = React.useRef<HTMLDivElement>(null);
40
+ const containerRef = React.useRef<HTMLDivElement>(null);
40
41
 
41
42
  const batchSize = 50;
42
43
 
@@ -82,6 +83,9 @@ export const ConsoleLogsList = () => {
82
83
  if (processFilter !== 'all') {
83
84
  filter.process = processFilter;
84
85
  }
86
+ if (searchText()) {
87
+ filter.message = { $matchWords: searchText() }
88
+ }
85
89
  return filter;
86
90
  };
87
91
 
@@ -90,15 +94,6 @@ export const ConsoleLogsList = () => {
90
94
  const table = await ConsoleLogs();
91
95
  const filter = buildFilter();
92
96
  let count = await table.count(filter);
93
-
94
- // If search text is applied, we need to count manually since it's client-side filtering
95
- if (searchText) {
96
- const allLogs = await table.list(filter);
97
- count = allLogs.filter(log =>
98
- log.message.toLowerCase().includes(searchText.toLowerCase())
99
- ).length;
100
- }
101
-
102
97
  setTotalLogCount(count);
103
98
  }
104
99
 
@@ -106,63 +101,57 @@ export const ConsoleLogsList = () => {
106
101
  async function fetchLogs(lastLog?: IConsoleLog): Promise<IConsoleLog[]> {
107
102
  const table = await ConsoleLogs();
108
103
  const filter: any = buildFilter();
109
-
110
104
  if (lastLog) {
111
105
  filter.logId = { $lt: lastLog.logId };
112
106
  }
113
-
114
- const cursor = table.cursor(filter, {
115
- sortBy: ['-timestamp', '-logId'],
116
- });
117
-
118
- const fetchedLogs: IConsoleLog[] = [];
119
- for await (const log of cursor) {
120
- // Apply text search filter (if search is implemented in cursor, this can be removed)
121
- if (searchText && !log.message.toLowerCase().includes(searchText.toLowerCase())) {
122
- continue;
123
- }
124
- fetchedLogs.push(log);
125
- if (fetchedLogs.length >= batchSize) {
126
- break;
127
- }
128
- }
129
- return fetchedLogs;
107
+ const results = await table.list(filter, { pageSize: batchSize, sortBy: ['-timestamp', '-logId'] });
108
+ return results;
130
109
  }
131
110
 
132
111
  // Load older logs (prepend to list)
133
- function prependLogs() {
112
+ async function loadMoreLogs(startLoadId?: string) {
113
+ if (startLoadId && startLoadId !== loadMoreId()) {
114
+ return;
115
+ }
116
+ startLoadId ??= loadMoreId();
134
117
  const oldestLog = logs()[0];
135
- fetchLogs(oldestLog).then(fetchedLogs => {
136
- if (fetchedLogs.length === 0) {
137
- setAllLogsLoaded(true);
138
- } else {
139
- let _logs = sortBy([...logs(), ...fetchedLogs], 'timestamp');
140
- _logs = uniqBy(_logs, l => l.logId);
141
- logs(_logs);
142
- }
143
- });
144
- return false;
118
+ const fetchedLogs = await fetchLogs(oldestLog);
119
+ if (loadMoreId() !== startLoadId) {
120
+ loadMoreLogs(loadMoreId());
121
+ return;
122
+ }
123
+ if (fetchedLogs.length === 0) {
124
+ setAllLogsLoaded(true);
125
+ }
126
+ let _logs = sortBy([...logs(), ...fetchedLogs], 'timestamp');
127
+ _logs = uniqBy(_logs, l => l.logId);
128
+ logs(_logs);
145
129
  }
146
130
 
147
131
  // Initial load and ensure screen is filled
148
132
  const minHeightOfLog = 30;
149
133
  useEffect(() => {
150
134
  if (!allLogsLoaded && (!logs.length || logs.length * minHeightOfLog < windowHeight())) {
151
- prependLogs();
135
+ loadMoreLogs();
152
136
  }
153
- }, [logs, levelFilter, processFilter, searchText]);
137
+ }, [logs, levelFilter, processFilter, searchText()]);
154
138
 
155
139
  // Reset when filters change
156
140
  useEffect(() => {
141
+ loadMoreId(newid());
157
142
  logs([]);
158
- setAllLogsLoaded(false);
159
143
  updateLogCount();
160
- }, [levelFilter, processFilter, searchText]);
144
+ if (allLogsLoaded) {
145
+ setAllLogsLoaded(false);
146
+ loadMoreLogs()
147
+ }
148
+ }, [levelFilter, processFilter, searchText()]);
161
149
 
162
150
  // Subscribe to new logs
163
151
  useEffect(() => {
152
+ let sub: ISubscriptionResult | undefined = undefined;
164
153
  ConsoleLogs().then(table => {
165
- const sub = table.dataChanged.subscribe(evt => {
154
+ sub = table.dataChanged.subscribe(evt => {
166
155
  // Update count whenever data changes
167
156
  updateLogCount();
168
157
 
@@ -171,7 +160,11 @@ export const ConsoleLogsList = () => {
171
160
  // Check if log matches current filters
172
161
  if (levelFilter !== 'all' && log.level !== levelFilter) return;
173
162
  if (processFilter !== 'all' && log.process !== processFilter) return;
174
- if (searchText && !log.message.toLowerCase().includes(searchText.toLowerCase())) return;
163
+ if (searchText()) {
164
+ const logMessage = log.message.toLowerCase();
165
+ const filterOut = searchText().toLowerCase().split(' ').some(word => !logMessage.includes(word));
166
+ if (filterOut) return;
167
+ }
175
168
 
176
169
  // Don't add we're only showing a limited batch and this is older
177
170
  if (logs().length > batchSize && min(logs().map(l => l.timestamp))! > log.timestamp) return;
@@ -183,12 +176,11 @@ export const ConsoleLogsList = () => {
183
176
  scrollToBottom('smooth');
184
177
  }
185
178
  });
186
-
187
- return () => {
188
- sub.unsubscribe();
189
- };
190
179
  });
191
- }, [levelFilter, processFilter, searchText]);
180
+ return () => {
181
+ sub?.unsubscribe();
182
+ };
183
+ }, [levelFilter, processFilter]);
192
184
 
193
185
  function scrollToBottom(behavior: 'instant' | 'smooth', delay = 100) {
194
186
  setTimeout(() => {
@@ -202,7 +194,6 @@ export const ConsoleLogsList = () => {
202
194
  const table = await ConsoleLogs();
203
195
  await table.deleteOldLogs(Date.now());
204
196
  logs([]);
205
- setAllLogsLoaded(true);
206
197
  setTotalLogCount(0);
207
198
  } catch (err) {
208
199
  console.error('Failed to clear logs:', err);
@@ -219,8 +210,8 @@ export const ConsoleLogsList = () => {
219
210
  setLevelFilter={setLevelFilter}
220
211
  processFilter={processFilter}
221
212
  setProcessFilter={setProcessFilter}
222
- searchText={searchText}
223
- setSearchText={setSearchText}
213
+ searchText={searchText()}
214
+ setSearchText={searchText}
224
215
  />
225
216
 
226
217
  <div
@@ -254,7 +245,7 @@ export const ConsoleLogsList = () => {
254
245
  >
255
246
  <InfiniteScroll
256
247
  dataLength={_logs.length}
257
- next={prependLogs}
248
+ next={loadMoreLogs}
258
249
  style={{ display: 'flex', flexDirection: 'column-reverse', overflow: 'hidden' }}
259
250
  inverse={true}
260
251
  hasMore={!allLogsLoaded}
@@ -12,7 +12,9 @@ import {
12
12
  valueTypeMentionConfig,
13
13
  IMentionConfig
14
14
  } from '../../mention-configs';
15
- import { IMentionData } from "@peers-app/peers-sdk";
15
+ import { IMentionData, IAppNav } from "@peers-app/peers-sdk";
16
+ import { allPackages } from '../../ui-router/routes-loader';
17
+ import { systemPackage } from '../../system-apps';
16
18
 
17
19
  interface SearchResult {
18
20
  config: IMentionConfig;
@@ -20,10 +22,22 @@ interface SearchResult {
20
22
  category: string;
21
23
  }
22
24
 
25
+ interface AppSearchItem {
26
+ packageId: string;
27
+ packageName: string;
28
+ navItem: IAppNav;
29
+ path: string;
30
+ name: string;
31
+ displayName: string;
32
+ iconClassName: string;
33
+ }
34
+
23
35
  export function GlobalSearch() {
24
36
  const [_colorMode] = useObservable(colorMode);
37
+ const [packages] = useObservable(allPackages);
25
38
  const [searchQuery, setSearchQuery] = useState('');
26
39
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
40
+ const [appResults, setAppResults] = useState<AppSearchItem[]>([]);
27
41
  const [isSearching, setIsSearching] = useState(false);
28
42
  const inputRef = useRef<HTMLInputElement>(null);
29
43
 
@@ -47,10 +61,42 @@ export function GlobalSearch() {
47
61
  { config: userMentionConfig, category: 'Users', navigationPath: 'profile' },
48
62
  ];
49
63
 
64
+ // Get all apps (system and user)
65
+ const getAllApps = (): AppSearchItem[] => {
66
+ const allPackages_ = [...packages, systemPackage];
67
+ return allPackages_
68
+ .filter(p => !p.disabled && p.appNavs && p.appNavs.length > 0)
69
+ .flatMap(pkg =>
70
+ pkg.appNavs!.map(navItem => {
71
+ // Construct path - use direct path for system apps, package-nav for others
72
+ let path: string;
73
+ if (pkg.packageId === 'system-apps') {
74
+ path = navItem.navigationPath ?? navItem.name.replace(/\s/g, '-').toLowerCase();
75
+ } else {
76
+ path = `package-nav/${pkg.packageId}/${(navItem.navigationPath ?? navItem.name).replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`;
77
+ while (path.includes('//')) {
78
+ path = path.replace('//', '/');
79
+ }
80
+ }
81
+
82
+ return {
83
+ packageId: pkg.packageId,
84
+ packageName: pkg.name,
85
+ navItem,
86
+ path,
87
+ name: navItem.name,
88
+ displayName: navItem.displayName || navItem.name,
89
+ iconClassName: navItem.iconClassName || 'bi-box-seam'
90
+ };
91
+ })
92
+ );
93
+ };
94
+
50
95
  // Debounced search effect
51
96
  useEffect(() => {
52
97
  if (!searchQuery.trim()) {
53
98
  setSearchResults([]);
99
+ setAppResults([]);
54
100
  return;
55
101
  }
56
102
 
@@ -75,13 +121,23 @@ export function GlobalSearch() {
75
121
  const filteredResults = searchResults.filter(Boolean) as SearchResult[];
76
122
 
77
123
  setSearchResults(filteredResults);
124
+
125
+ // Search apps
126
+ const allApps = getAllApps();
127
+ const lowerQuery = searchQuery.toLowerCase();
128
+ const filteredApps = allApps.filter(app =>
129
+ app.name.toLowerCase().includes(lowerQuery) ||
130
+ app.displayName.toLowerCase().includes(lowerQuery) ||
131
+ app.packageName.toLowerCase().includes(lowerQuery)
132
+ );
133
+ setAppResults(filteredApps);
78
134
  } finally {
79
135
  setIsSearching(false);
80
136
  }
81
137
  }, 300); // 300ms debounce
82
138
 
83
139
  return () => clearTimeout(timeoutId);
84
- }, [searchQuery]);
140
+ }, [searchQuery, packages]);
85
141
 
86
142
  const handleItemClick = (result: SearchResult, item: IMentionData) => {
87
143
  // Try using the config's onClick first
@@ -103,7 +159,11 @@ export function GlobalSearch() {
103
159
  }
104
160
  };
105
161
 
106
- const totalResults = searchResults.reduce((sum, result) => sum + result.items.length, 0);
162
+ const handleAppClick = (app: AppSearchItem) => {
163
+ goToTabPath(app.path);
164
+ };
165
+
166
+ const totalResults = searchResults.reduce((sum, result) => sum + result.items.length, 0) + appResults.length;
107
167
 
108
168
  return (
109
169
  <div className="container-fluid h-100 p-4" style={{ maxHeight: '100vh', overflowY: 'auto' }}>
@@ -131,7 +191,7 @@ export function GlobalSearch() {
131
191
  ref={inputRef}
132
192
  type="text"
133
193
  className={`form-control form-control-lg ${isDark ? 'bg-dark text-light border-secondary' : ''}`}
134
- placeholder="Search across tools, assistants, workflows, events, and more..."
194
+ placeholder="Search across apps, tools, assistants, workflows, events, and more..."
135
195
  value={searchQuery}
136
196
  onChange={(e) => setSearchQuery(e.target.value)}
137
197
  style={{
@@ -162,7 +222,7 @@ export function GlobalSearch() {
162
222
  {isSearching ? (
163
223
  'Searching...'
164
224
  ) : totalResults > 0 ? (
165
- `Found ${totalResults} result${totalResults !== 1 ? 's' : ''} across ${searchResults.length} categor${searchResults.length !== 1 ? 'ies' : 'y'}`
225
+ `Found ${totalResults} result${totalResults !== 1 ? 's' : ''}`
166
226
  ) : searchQuery.trim() ? (
167
227
  'No results found'
168
228
  ) : null}
@@ -173,7 +233,7 @@ export function GlobalSearch() {
173
233
  {/* Search Results */}
174
234
  {searchQuery && !isSearching && (
175
235
  <div>
176
- {searchResults.length === 0 ? (
236
+ {searchResults.length === 0 && appResults.length === 0 ? (
177
237
  <div className="text-center py-5">
178
238
  <i className="bi-search mb-3 d-block text-muted" style={{ fontSize: '48px' }} />
179
239
  <h4 className="text-muted">No results found</h4>
@@ -183,6 +243,65 @@ export function GlobalSearch() {
183
243
  </div>
184
244
  ) : (
185
245
  <div>
246
+ {/* Apps Section */}
247
+ {appResults.length > 0 && (
248
+ <div className="mb-5">
249
+ <div className="d-flex align-items-center mb-3">
250
+ <i className="bi-grid-3x3-gap me-3" style={{ fontSize: '20px', color: '#6c757d' }} />
251
+ <h4 className="mb-0 me-3">Apps</h4>
252
+ <span className="badge bg-secondary">{appResults.length}</span>
253
+ </div>
254
+ <div className="row g-3">
255
+ {appResults.map((app) => (
256
+ <div key={`${app.packageId}-${app.path}`} className="col-12 col-md-6 col-lg-4">
257
+ <div
258
+ className={`card h-100 ${isDark ? 'bg-dark border-secondary' : 'bg-light'}`}
259
+ style={{
260
+ cursor: 'pointer',
261
+ transition: 'all 0.15s ease',
262
+ borderRadius: '8px'
263
+ }}
264
+ onClick={() => handleAppClick(app)}
265
+ onMouseEnter={(e) => {
266
+ e.currentTarget.style.transform = 'translateY(-2px)';
267
+ e.currentTarget.style.boxShadow = isDark
268
+ ? '0 4px 12px rgba(0,0,0,0.3)'
269
+ : '0 4px 12px rgba(0,0,0,0.1)';
270
+ }}
271
+ onMouseLeave={(e) => {
272
+ e.currentTarget.style.transform = 'translateY(0)';
273
+ e.currentTarget.style.boxShadow = 'none';
274
+ }}
275
+ >
276
+ <div className="card-body p-3">
277
+ <div className="d-flex align-items-start">
278
+ <i
279
+ className={`${app.iconClassName} me-3 mt-1`}
280
+ style={{
281
+ fontSize: '16px',
282
+ color: isDark ? '#0d6efd' : '#0d6efd',
283
+ minWidth: '16px'
284
+ }}
285
+ />
286
+ <div className="flex-grow-1">
287
+ <h6 className="card-title mb-1 fw-medium">
288
+ {app.displayName}
289
+ </h6>
290
+ <small className="text-muted text-uppercase" style={{ fontSize: '11px', letterSpacing: '0.5px' }}>
291
+ {app.packageId === 'system-apps' ? 'System App' : app.packageName}
292
+ </small>
293
+ </div>
294
+ <i className="bi-arrow-right text-muted" style={{ fontSize: '12px' }} />
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ ))}
300
+ </div>
301
+ </div>
302
+ )}
303
+
304
+ {/* Entity Results */}
186
305
  {searchResults.map((result) => (
187
306
  <div key={result.category} className="mb-5">
188
307
  {/* Category Header */}
@@ -253,7 +372,7 @@ export function GlobalSearch() {
253
372
  <i className="bi-search mb-3 d-block text-muted" style={{ fontSize: '64px' }} />
254
373
  <h3 className="text-muted mb-3">Search across everything</h3>
255
374
  <p className="text-muted mb-4" style={{ maxWidth: '400px', margin: '0 auto' }}>
256
- Find tools, assistants, workflows, events, predicates, types, and users all in one place.
375
+ Find apps, tools, assistants, workflows, events, predicates, types, and users all in one place.
257
376
  </p>
258
377
  <div className="d-flex flex-wrap justify-content-center gap-2">
259
378
  {searchConfigs.map(({ config, category }) => (