@unbrained/pm-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// ROUTER — View switching with URL routing
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { VIEW_NAMES } from '../constants.js';
|
|
6
|
+
import { renderProjectsView } from './projects.js';
|
|
7
|
+
import { renderItemsView } from './items.js';
|
|
8
|
+
import { renderCreateView } from './create.js';
|
|
9
|
+
import { renderActivityView } from './activity.js';
|
|
10
|
+
import { renderSearchView } from './search.js';
|
|
11
|
+
import { renderStatsView } from './stats.js';
|
|
12
|
+
import { renderCalendarView } from './calendar.js';
|
|
13
|
+
import { renderContextView } from './context.js';
|
|
14
|
+
import { renderGraphView } from './graph.js';
|
|
15
|
+
import { renderSharingView } from './sharing.js';
|
|
16
|
+
import { renderGroupsView } from './groups.js';
|
|
17
|
+
import { renderHealthView } from './health.js';
|
|
18
|
+
import { renderDedupeAuditView } from './dedupe.js';
|
|
19
|
+
import { renderValidateView } from './validate.js';
|
|
20
|
+
import { renderSettingsView } from './settings.js';
|
|
21
|
+
import { renderGitHubView } from './github.js';
|
|
22
|
+
import { renderExportView } from './export.js';
|
|
23
|
+
import { renderNormalizeView } from './normalize.js';
|
|
24
|
+
import { renderSharedView } from './shared.js';
|
|
25
|
+
import { renderTemplatesView } from './templates.js';
|
|
26
|
+
import { renderCommentsAuditView } from './comments-audit.js';
|
|
27
|
+
import { renderConfigView } from './config.js';
|
|
28
|
+
import { renderGuideView } from './guide.js';
|
|
29
|
+
import { renderAdminView } from './admin.js';
|
|
30
|
+
import { initPlanView } from './plan.js';
|
|
31
|
+
|
|
32
|
+
// View name → URL path mapping
|
|
33
|
+
const VIEW_TO_PATH: Record<string, string> = {
|
|
34
|
+
'projects': '/',
|
|
35
|
+
'items': '/items',
|
|
36
|
+
'create': '/create',
|
|
37
|
+
'activity': '/activity',
|
|
38
|
+
'search': '/search',
|
|
39
|
+
'stats': '/stats',
|
|
40
|
+
'calendar': '/calendar',
|
|
41
|
+
'context': '/context',
|
|
42
|
+
'graph': '/graph',
|
|
43
|
+
'sharing': '/sharing',
|
|
44
|
+
'groups': '/groups',
|
|
45
|
+
'health': '/health',
|
|
46
|
+
'dedupe': '/dedupe',
|
|
47
|
+
'validate': '/validate',
|
|
48
|
+
'settings': '/settings',
|
|
49
|
+
'github': '/github',
|
|
50
|
+
'export': '/export',
|
|
51
|
+
'normalize': '/normalize',
|
|
52
|
+
'shared': '/shared',
|
|
53
|
+
'templates': '/templates',
|
|
54
|
+
'comments-audit': '/comments-audit',
|
|
55
|
+
'config': '/config',
|
|
56
|
+
'guide': '/guide',
|
|
57
|
+
'admin': '/admin',
|
|
58
|
+
'plan': '/plan',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Reverse: URL path → view name
|
|
62
|
+
const PATH_TO_VIEW: Record<string, string> = {};
|
|
63
|
+
for (const [view, path] of Object.entries(VIEW_TO_PATH)) {
|
|
64
|
+
PATH_TO_VIEW[path] = view;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Whether pushState was just called (to ignore the resulting popstate)
|
|
68
|
+
let navigatingInternally = false;
|
|
69
|
+
|
|
70
|
+
// Callback invoked when the view changes — used to notify presence service
|
|
71
|
+
let onViewChange: ((view: string) => void) | null = null;
|
|
72
|
+
export function setOnViewChange(cb: (view: string) => void): void {
|
|
73
|
+
onViewChange = cb;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getPathForView(view: string): string {
|
|
77
|
+
return VIEW_TO_PATH[view] || '/';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getViewForPath(path: string): string {
|
|
81
|
+
// Normalize: remove trailing slash except for root
|
|
82
|
+
const normalized = path.replace(/\/$/, '') || '/';
|
|
83
|
+
// Direct match
|
|
84
|
+
if (PATH_TO_VIEW[normalized]) return PATH_TO_VIEW[normalized];
|
|
85
|
+
// Try matching first segment for deeper paths (e.g. /items/DETAIL-1 → items)
|
|
86
|
+
const firstSegment = '/' + normalized.slice(1).split('/')[0];
|
|
87
|
+
return PATH_TO_VIEW[firstSegment] || 'projects';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function showView(view: string, pushState = true): void {
|
|
91
|
+
if (view === 'admin' && !state.user?.is_admin) {
|
|
92
|
+
history.replaceState({ view: 'projects' }, '', '/');
|
|
93
|
+
view = 'projects';
|
|
94
|
+
}
|
|
95
|
+
state.currentView = view;
|
|
96
|
+
|
|
97
|
+
// Update URL via pushState
|
|
98
|
+
if (pushState) {
|
|
99
|
+
const path = getPathForView(view);
|
|
100
|
+
if (window.location.pathname !== path) {
|
|
101
|
+
navigatingInternally = true;
|
|
102
|
+
history.pushState({ view }, '', path);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Full-screen graph mode: hide sidebar
|
|
107
|
+
document.body.classList.toggle('graph-mode', view === 'graph');
|
|
108
|
+
VIEW_NAMES.forEach(v => {
|
|
109
|
+
const el = document.getElementById(`content-${v}`);
|
|
110
|
+
if (el) el.style.display = v === view ? '' : 'none';
|
|
111
|
+
});
|
|
112
|
+
document.querySelectorAll('.sidebar-item[data-view]').forEach(el => {
|
|
113
|
+
(el as HTMLElement).classList.toggle('active', (el as HTMLElement).dataset.view === view);
|
|
114
|
+
});
|
|
115
|
+
if (view === 'projects') {
|
|
116
|
+
document.querySelectorAll('#sidebar-projects-section .sidebar-item').forEach((el,i) => {
|
|
117
|
+
(el as HTMLElement).classList.toggle('active', i===0);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
switch(view) {
|
|
121
|
+
case 'projects': renderProjectsView(); break;
|
|
122
|
+
case 'items': renderItemsView(); break;
|
|
123
|
+
case 'create': renderCreateView(); break;
|
|
124
|
+
case 'activity': renderActivityView(); break;
|
|
125
|
+
case 'search': renderSearchView(); break;
|
|
126
|
+
case 'stats': renderStatsView(); break;
|
|
127
|
+
case 'calendar': renderCalendarView(); break;
|
|
128
|
+
case 'context': renderContextView(); break;
|
|
129
|
+
case 'graph': renderGraphView(); break;
|
|
130
|
+
case 'sharing': renderSharingView(); break;
|
|
131
|
+
case 'groups': renderGroupsView(); break;
|
|
132
|
+
case 'health': renderHealthView(); break;
|
|
133
|
+
case 'dedupe': renderDedupeAuditView(); break;
|
|
134
|
+
case 'validate': renderValidateView(); break;
|
|
135
|
+
case 'settings': renderSettingsView(); break;
|
|
136
|
+
case 'github': renderGitHubView(); break;
|
|
137
|
+
case 'export': renderExportView(); break;
|
|
138
|
+
case 'normalize': renderNormalizeView(); break;
|
|
139
|
+
case 'shared': renderSharedView(); break;
|
|
140
|
+
case 'templates': renderTemplatesView(); break;
|
|
141
|
+
case 'comments-audit': renderCommentsAuditView(); break;
|
|
142
|
+
case 'config': renderConfigView(); break;
|
|
143
|
+
case 'guide': renderGuideView(); break;
|
|
144
|
+
case 'admin': renderAdminView(); break;
|
|
145
|
+
case 'plan': void initPlanView(); break;
|
|
146
|
+
}
|
|
147
|
+
updateMobileNav(view);
|
|
148
|
+
|
|
149
|
+
// Notify presence service of view change
|
|
150
|
+
onViewChange?.(view);
|
|
151
|
+
|
|
152
|
+
// Scroll main content to top on view change
|
|
153
|
+
const mainContent = document.getElementById('main-content');
|
|
154
|
+
if (mainContent) mainContent.scrollTop = 0;
|
|
155
|
+
|
|
156
|
+
// Move focus to main content for accessibility
|
|
157
|
+
const activeArea = document.getElementById(`content-${view}`);
|
|
158
|
+
if (activeArea) {
|
|
159
|
+
activeArea.setAttribute('tabindex', '-1');
|
|
160
|
+
activeArea.focus({ preventScroll: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function updateMobileNav(view: string): void {
|
|
165
|
+
document.querySelectorAll('.mobile-bottom-nav-item').forEach(el => {
|
|
166
|
+
(el as HTMLElement).classList.toggle('active', (el as HTMLElement).dataset.mobview === view);
|
|
167
|
+
});
|
|
168
|
+
const nav = document.getElementById('mobile-bottom-nav');
|
|
169
|
+
if (nav) {
|
|
170
|
+
nav.classList.toggle('visible', !!state.currentProject && view !== 'projects');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle browser back/forward
|
|
175
|
+
function onPopState(e: PopStateEvent): void {
|
|
176
|
+
// Ignore if we just pushed state (some browsers fire popstate after pushState)
|
|
177
|
+
if (navigatingInternally) {
|
|
178
|
+
navigatingInternally = false;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const view = e.state?.view || getViewForPath(window.location.pathname);
|
|
182
|
+
showView(view, false);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Initialize popstate listener
|
|
186
|
+
if (typeof window !== 'undefined') {
|
|
187
|
+
window.addEventListener('popstate', onPopState);
|
|
188
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SEARCH VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { toast } from '../components/toast.js';
|
|
8
|
+
import { renderItemRow } from './items.js';
|
|
9
|
+
let searchTimer;
|
|
10
|
+
export function renderSearchView() {
|
|
11
|
+
const el = document.getElementById('content-search');
|
|
12
|
+
if (!el)
|
|
13
|
+
return;
|
|
14
|
+
if (!state.currentProject) {
|
|
15
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const modeOpts = [
|
|
19
|
+
{ val: 'hybrid', label: 'Hybrid', tip: 'Keyword + semantic (best)' },
|
|
20
|
+
{ val: 'semantic', label: 'Semantic', tip: 'Ollama qwen3 embeddings' },
|
|
21
|
+
{ val: 'keyword', label: 'Keyword', tip: 'Fast exact match' },
|
|
22
|
+
];
|
|
23
|
+
el.innerHTML = `
|
|
24
|
+
<div class="page-header">
|
|
25
|
+
<div><div class="page-title">Search</div><div class="page-subtitle">Search items in ${escHtml(state.currentProject.name)}</div></div>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap">
|
|
28
|
+
<div style="display:flex;gap:4px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:3px">
|
|
29
|
+
${modeOpts.map(m => `<button class="btn btn-sm" style="padding:4px 10px;border-radius:5px;font-size:12px;transition:var(--transition);${state.searchMode === m.val ? 'background:var(--bg-card);color:var(--text-primary)' : 'background:transparent;color:var(--text-muted)'}" onclick="window.__app.setSearchMode('${m.val}')" title="${m.tip}">${m.label}</button>`).join('')}
|
|
30
|
+
</div>
|
|
31
|
+
<button class="btn btn-secondary btn-sm" id="reindex-btn" onclick="window.__app.reindexProject()">⟳ Reindex</button>
|
|
32
|
+
<span style="font-size:11px;color:var(--text-muted)">Semantic powered by Ollama qwen3</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="search-box-wrap">
|
|
35
|
+
<span class="search-icon">⌕</span>
|
|
36
|
+
<input class="search-input" id="search-query" type="text" placeholder="Search items by title, description, tags…" value="${escHtml(state.searchQuery)}" oninput="window.__app.debouncedSearch()" onkeydown="if(event.key==='Enter')window.__app.doSearch()">
|
|
37
|
+
</div>
|
|
38
|
+
<div id="search-results">
|
|
39
|
+
${state.searchResults.length > 0 ? renderSearchResults() : '<div class="empty-state"><div class="empty-state-icon">⌕</div><div class="empty-state-text">Enter a query to search</div></div>'}
|
|
40
|
+
</div>`;
|
|
41
|
+
setTimeout(() => document.getElementById('search-query')?.focus(), 50);
|
|
42
|
+
}
|
|
43
|
+
export function setSearchMode(mode) {
|
|
44
|
+
state.searchMode = mode;
|
|
45
|
+
state.searchResults = [];
|
|
46
|
+
renderSearchView();
|
|
47
|
+
if (state.searchQuery)
|
|
48
|
+
doSearch();
|
|
49
|
+
}
|
|
50
|
+
export async function reindexProject() {
|
|
51
|
+
if (!state.currentProject)
|
|
52
|
+
return;
|
|
53
|
+
const btn = document.getElementById('reindex-btn');
|
|
54
|
+
if (!btn)
|
|
55
|
+
return;
|
|
56
|
+
btn.disabled = true;
|
|
57
|
+
btn.textContent = '⟳ Reindexing…';
|
|
58
|
+
const mode = state.searchMode;
|
|
59
|
+
try {
|
|
60
|
+
await api('POST', `/projects/${state.currentProject.id}/pm/reindex`, { mode });
|
|
61
|
+
toast(`Reindex complete (${mode})`, 'success');
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
toast(`Reindex failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
btn.disabled = false;
|
|
68
|
+
btn.textContent = '⟳ Reindex';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function debouncedSearch() {
|
|
72
|
+
clearTimeout(searchTimer);
|
|
73
|
+
state.searchQuery = document.getElementById('search-query')?.value || '';
|
|
74
|
+
searchTimer = setTimeout(doSearch, 350);
|
|
75
|
+
}
|
|
76
|
+
export async function doSearch() {
|
|
77
|
+
const query = (document.getElementById('search-query')?.value || '').trim();
|
|
78
|
+
if (!query || !state.currentProject)
|
|
79
|
+
return;
|
|
80
|
+
state.searchQuery = query;
|
|
81
|
+
const resultsEl = document.getElementById('search-results');
|
|
82
|
+
if (resultsEl)
|
|
83
|
+
resultsEl.innerHTML = '<div class="loading-state"><div class="loading-spinner"></div></div>';
|
|
84
|
+
try {
|
|
85
|
+
const data = await api('POST', `/projects/${state.currentProject.id}/pm/search`, { query, mode: state.searchMode });
|
|
86
|
+
state.searchResults = data.results || data.items || [];
|
|
87
|
+
if (resultsEl)
|
|
88
|
+
resultsEl.innerHTML = state.searchResults.length === 0
|
|
89
|
+
? `<div class="empty-state"><div class="empty-state-text">No results for "${escHtml(query)}"</div></div>`
|
|
90
|
+
: renderSearchResults();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (resultsEl)
|
|
94
|
+
resultsEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function renderSearchResults() {
|
|
98
|
+
if (!state.searchResults.length)
|
|
99
|
+
return '';
|
|
100
|
+
return `<div style="color:var(--text-muted);font-size:12px;margin-bottom:8px">${state.searchResults.length} result${state.searchResults.length !== 1 ? 's' : ''}</div>
|
|
101
|
+
<div class="item-list">${state.searchResults.map(item => renderItemRow(item)).join('')}</div>`;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SEARCH VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { toast } from '../components/toast.js';
|
|
8
|
+
import { renderItemRow } from './items.js';
|
|
9
|
+
|
|
10
|
+
let searchTimer: ReturnType<typeof setTimeout>;
|
|
11
|
+
|
|
12
|
+
export function renderSearchView(): void {
|
|
13
|
+
const el = document.getElementById('content-search');
|
|
14
|
+
if (!el) return;
|
|
15
|
+
if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
|
|
16
|
+
const modeOpts = [
|
|
17
|
+
{ val: 'hybrid', label: 'Hybrid', tip: 'Keyword + semantic (best)' },
|
|
18
|
+
{ val: 'semantic', label: 'Semantic', tip: 'Ollama qwen3 embeddings' },
|
|
19
|
+
{ val: 'keyword', label: 'Keyword', tip: 'Fast exact match' },
|
|
20
|
+
];
|
|
21
|
+
el.innerHTML = `
|
|
22
|
+
<div class="page-header">
|
|
23
|
+
<div><div class="page-title">Search</div><div class="page-subtitle">Search items in ${escHtml(state.currentProject.name)}</div></div>
|
|
24
|
+
</div>
|
|
25
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap">
|
|
26
|
+
<div style="display:flex;gap:4px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);padding:3px">
|
|
27
|
+
${modeOpts.map(m=>`<button class="btn btn-sm" style="padding:4px 10px;border-radius:5px;font-size:12px;transition:var(--transition);${state.searchMode===m.val?'background:var(--bg-card);color:var(--text-primary)':'background:transparent;color:var(--text-muted)'}" onclick="window.__app.setSearchMode('${m.val}')" title="${m.tip}">${m.label}</button>`).join('')}
|
|
28
|
+
</div>
|
|
29
|
+
<button class="btn btn-secondary btn-sm" id="reindex-btn" onclick="window.__app.reindexProject()">⟳ Reindex</button>
|
|
30
|
+
<span style="font-size:11px;color:var(--text-muted)">Semantic powered by Ollama qwen3</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="search-box-wrap">
|
|
33
|
+
<span class="search-icon">⌕</span>
|
|
34
|
+
<input class="search-input" id="search-query" type="text" placeholder="Search items by title, description, tags…" value="${escHtml(state.searchQuery)}" oninput="window.__app.debouncedSearch()" onkeydown="if(event.key==='Enter')window.__app.doSearch()">
|
|
35
|
+
</div>
|
|
36
|
+
<div id="search-results">
|
|
37
|
+
${state.searchResults.length > 0 ? renderSearchResults() : '<div class="empty-state"><div class="empty-state-icon">⌕</div><div class="empty-state-text">Enter a query to search</div></div>'}
|
|
38
|
+
</div>`;
|
|
39
|
+
setTimeout(()=>document.getElementById('search-query')?.focus(),50);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setSearchMode(mode: string): void {
|
|
43
|
+
state.searchMode = mode;
|
|
44
|
+
state.searchResults = [];
|
|
45
|
+
renderSearchView();
|
|
46
|
+
if (state.searchQuery) doSearch();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function reindexProject(): Promise<void> {
|
|
50
|
+
if (!state.currentProject) return;
|
|
51
|
+
const btn = document.getElementById('reindex-btn') as HTMLButtonElement | null;
|
|
52
|
+
if (!btn) return;
|
|
53
|
+
btn.disabled = true;
|
|
54
|
+
btn.textContent = '⟳ Reindexing…';
|
|
55
|
+
const mode = state.searchMode as string;
|
|
56
|
+
try {
|
|
57
|
+
await api('POST',`/projects/${state.currentProject.id}/pm/reindex`,{mode});
|
|
58
|
+
toast(`Reindex complete (${mode})`, 'success');
|
|
59
|
+
} catch(err: unknown) {
|
|
60
|
+
toast(`Reindex failed: ${err instanceof Error ? err.message : String(err)}`,'error');
|
|
61
|
+
} finally {
|
|
62
|
+
btn.disabled = false;
|
|
63
|
+
btn.textContent = '⟳ Reindex';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function debouncedSearch(): void {
|
|
68
|
+
clearTimeout(searchTimer);
|
|
69
|
+
state.searchQuery = (document.getElementById('search-query') as HTMLInputElement | null)?.value || '';
|
|
70
|
+
searchTimer = setTimeout(doSearch, 350);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function doSearch(): Promise<void> {
|
|
74
|
+
const query = ((document.getElementById('search-query') as HTMLInputElement | null)?.value || '').trim();
|
|
75
|
+
if (!query || !state.currentProject) return;
|
|
76
|
+
state.searchQuery = query;
|
|
77
|
+
const resultsEl = document.getElementById('search-results');
|
|
78
|
+
if (resultsEl) resultsEl.innerHTML = '<div class="loading-state"><div class="loading-spinner"></div></div>';
|
|
79
|
+
try {
|
|
80
|
+
const data = await api('POST',`/projects/${state.currentProject.id}/pm/search`,{query,mode:state.searchMode});
|
|
81
|
+
state.searchResults = (data as any).results || (data as any).items || [];
|
|
82
|
+
if (resultsEl) resultsEl.innerHTML = state.searchResults.length === 0
|
|
83
|
+
? `<div class="empty-state"><div class="empty-state-text">No results for "${escHtml(query)}"</div></div>`
|
|
84
|
+
: renderSearchResults();
|
|
85
|
+
} catch(err: unknown) {
|
|
86
|
+
if (resultsEl) resultsEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderSearchResults(): string {
|
|
91
|
+
if (!state.searchResults.length) return '';
|
|
92
|
+
return `<div style="color:var(--text-muted);font-size:12px;margin-bottom:8px">${state.searchResults.length} result${state.searchResults.length!==1?'s':''}</div>
|
|
93
|
+
<div class="item-list">${state.searchResults.map(item=>renderItemRow(item)).join('')}</div>`;
|
|
94
|
+
}
|