superintent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/bin/superintent.js +2 -0
- package/dist/commands/extract.d.ts +2 -0
- package/dist/commands/extract.js +66 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/knowledge.d.ts +2 -0
- package/dist/commands/knowledge.js +647 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +153 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +283 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/ticket.d.ts +4 -0
- package/dist/commands/ticket.js +942 -0
- package/dist/commands/ui.d.ts +2 -0
- package/dist/commands/ui.js +954 -0
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +26 -0
- package/dist/db/init-schema.d.ts +2 -0
- package/dist/db/init-schema.js +28 -0
- package/dist/db/parsers.d.ts +24 -0
- package/dist/db/parsers.js +79 -0
- package/dist/db/schema.d.ts +7 -0
- package/dist/db/schema.js +64 -0
- package/dist/db/usage.d.ts +8 -0
- package/dist/db/usage.js +24 -0
- package/dist/embed/model.d.ts +5 -0
- package/dist/embed/model.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +31 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +1 -0
- package/dist/ui/components/index.d.ts +6 -0
- package/dist/ui/components/index.js +13 -0
- package/dist/ui/components/knowledge.d.ts +33 -0
- package/dist/ui/components/knowledge.js +238 -0
- package/dist/ui/components/layout.d.ts +1 -0
- package/dist/ui/components/layout.js +323 -0
- package/dist/ui/components/search.d.ts +15 -0
- package/dist/ui/components/search.js +114 -0
- package/dist/ui/components/spec.d.ts +11 -0
- package/dist/ui/components/spec.js +253 -0
- package/dist/ui/components/ticket.d.ts +90 -0
- package/dist/ui/components/ticket.js +604 -0
- package/dist/ui/components/utils.d.ts +26 -0
- package/dist/ui/components/utils.js +34 -0
- package/dist/ui/styles.css +2 -0
- package/dist/utils/cli.d.ts +21 -0
- package/dist/utils/cli.js +31 -0
- package/dist/utils/config.d.ts +12 -0
- package/dist/utils/config.js +116 -0
- package/dist/utils/id.d.ts +6 -0
- package/dist/utils/id.js +13 -0
- package/dist/utils/io.d.ts +8 -0
- package/dist/utils/io.js +15 -0
- package/package.json +60 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Knowledge-related UI components
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
// Helper to render knowledge view
|
|
4
|
+
export function renderKnowledgeView() {
|
|
5
|
+
return `
|
|
6
|
+
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
|
|
7
|
+
<aside class="w-full lg:w-64 shrink-0 lg:sticky lg:top-4 lg:self-start">
|
|
8
|
+
<h2 class="text-2xl font-bold text-gray-800 mb-4">Filters</h2>
|
|
9
|
+
<div class="grid grid-cols-2 lg:grid-cols-1 gap-2 lg:gap-4 lg:space-y-0 bg-white rounded-lg shadow-card p-3 lg:p-4">
|
|
10
|
+
<div>
|
|
11
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
12
|
+
<select name="k-status" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
|
|
13
|
+
hx-get="/partials/knowledge-list"
|
|
14
|
+
hx-trigger="change"
|
|
15
|
+
hx-target="#knowledge-list"
|
|
16
|
+
hx-include="[name='k-category'],[name='k-namespace'],[name='k-scope'],[name='k-origin']">
|
|
17
|
+
<option value="active">Active</option>
|
|
18
|
+
<option value="inactive">Inactive</option>
|
|
19
|
+
<option value="all">All</option>
|
|
20
|
+
</select>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div>
|
|
24
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
|
25
|
+
<select name="k-category" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
|
|
26
|
+
hx-get="/partials/knowledge-list"
|
|
27
|
+
hx-trigger="change"
|
|
28
|
+
hx-target="#knowledge-list"
|
|
29
|
+
hx-include="[name='k-status'],[name='k-namespace'],[name='k-scope'],[name='k-origin']">
|
|
30
|
+
<option value="">All</option>
|
|
31
|
+
<option value="pattern">Pattern</option>
|
|
32
|
+
<option value="truth">Truth</option>
|
|
33
|
+
<option value="principle">Principle</option>
|
|
34
|
+
<option value="architecture">Architecture</option>
|
|
35
|
+
<option value="gotcha">Gotcha</option>
|
|
36
|
+
</select>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Scope</label>
|
|
41
|
+
<select name="k-scope" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
|
|
42
|
+
hx-get="/partials/knowledge-list"
|
|
43
|
+
hx-trigger="change"
|
|
44
|
+
hx-target="#knowledge-list"
|
|
45
|
+
hx-include="[name='k-status'],[name='k-category'],[name='k-namespace'],[name='k-origin']">
|
|
46
|
+
<option value="">All</option>
|
|
47
|
+
<option value="global">Global</option>
|
|
48
|
+
<option value="new-only">New Only</option>
|
|
49
|
+
<option value="backward-compatible">Backward Compatible</option>
|
|
50
|
+
<option value="legacy-frozen">Legacy Frozen</option>
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div>
|
|
55
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Source</label>
|
|
56
|
+
<select name="k-origin" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
|
|
57
|
+
hx-get="/partials/knowledge-list"
|
|
58
|
+
hx-trigger="change"
|
|
59
|
+
hx-target="#knowledge-list"
|
|
60
|
+
hx-include="[name='k-status'],[name='k-category'],[name='k-namespace'],[name='k-scope']">
|
|
61
|
+
<option value="">All</option>
|
|
62
|
+
<option value="ticket">Ticket</option>
|
|
63
|
+
<option value="discovery">Discovery</option>
|
|
64
|
+
<option value="manual">Manual</option>
|
|
65
|
+
</select>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</aside>
|
|
69
|
+
|
|
70
|
+
<main class="flex-1">
|
|
71
|
+
<h1 class="text-2xl font-bold text-gray-800 mb-4">Knowledge Base</h1>
|
|
72
|
+
<div id="knowledge-list" hx-get="/partials/knowledge-list" hx-trigger="load, poll-refresh" hx-swap="innerHTML">
|
|
73
|
+
</div>
|
|
74
|
+
</main>
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
// Helper to render knowledge list
|
|
79
|
+
export function renderKnowledgeList(items) {
|
|
80
|
+
if (items.length === 0) {
|
|
81
|
+
return '<p class="text-gray-500 text-center py-8">No knowledge entries found</p>';
|
|
82
|
+
}
|
|
83
|
+
const categoryColors = {
|
|
84
|
+
pattern: 'purple',
|
|
85
|
+
truth: 'green',
|
|
86
|
+
principle: 'orange',
|
|
87
|
+
architecture: 'blue',
|
|
88
|
+
gotcha: 'red',
|
|
89
|
+
};
|
|
90
|
+
return `
|
|
91
|
+
<div class="space-y-3">
|
|
92
|
+
${items.map(k => {
|
|
93
|
+
const color = categoryColors[k.category || ''] || 'gray';
|
|
94
|
+
const inactiveClass = !k.active ? 'opacity-60 border-dashed' : '';
|
|
95
|
+
return `
|
|
96
|
+
<div class="bg-white rounded-lg shadow-card p-4 hover:shadow-card-hover transition-shadow cursor-pointer ${inactiveClass}"
|
|
97
|
+
hx-get="/partials/knowledge-modal/${encodeURIComponent(k.id)}"
|
|
98
|
+
hx-target="#modal-content"
|
|
99
|
+
hx-trigger="click"
|
|
100
|
+
onclick="showModal()">
|
|
101
|
+
<div class="text-xs font-mono text-gray-400 mb-1">${escapeHtml(k.id)}</div>
|
|
102
|
+
<div class="flex items-start justify-between">
|
|
103
|
+
<div class="flex-1">
|
|
104
|
+
<div class="flex items-center gap-2">
|
|
105
|
+
<div class="text-sm font-medium text-gray-800">${escapeHtml(k.title)}</div>
|
|
106
|
+
${!k.active ? '<span class="px-2 py-0.5 text-xs rounded bg-gray-200 text-gray-600">Inactive</span>' : ''}
|
|
107
|
+
</div>
|
|
108
|
+
<p class="text-sm text-gray-600 mt-1 line-clamp-2">${escapeHtml(k.content.slice(0, 200))}${k.content.length > 200 ? '...' : ''}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="text-right flex-shrink-0 ml-4">
|
|
111
|
+
<div class="text-sm font-medium text-gray-600">${Math.round(k.confidence * 100)}%</div>
|
|
112
|
+
<div class="text-xs text-gray-400">confidence</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="flex items-center mt-3 text-xs text-gray-500 gap-3">
|
|
116
|
+
<span><span class="text-gray-400">Namespace:</span> ${escapeHtml(k.namespace)}</span>
|
|
117
|
+
<span><span class="text-gray-400">Source:</span> ${k.source || 'manual'}${k.source === 'ticket' && k.origin_ticket_type ? ` (${k.origin_ticket_type})` : ''}</span>
|
|
118
|
+
${k.category ? `<span><span class="text-gray-400">Category:</span> <span class="text-${color}-600 font-medium">${k.category}</span></span>` : ''}
|
|
119
|
+
<span><span class="text-gray-400">Scope:</span> ${k.decision_scope}</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
}).join('')}
|
|
124
|
+
</div>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
// Helper to render knowledge modal
|
|
128
|
+
export function renderKnowledgeModal(knowledge) {
|
|
129
|
+
const categoryColors = {
|
|
130
|
+
pattern: 'purple',
|
|
131
|
+
truth: 'green',
|
|
132
|
+
principle: 'orange',
|
|
133
|
+
architecture: 'blue',
|
|
134
|
+
gotcha: 'red',
|
|
135
|
+
};
|
|
136
|
+
const color = categoryColors[knowledge.category || ''] || 'gray';
|
|
137
|
+
return `
|
|
138
|
+
<div class="p-6">
|
|
139
|
+
<div class="flex items-start justify-between mb-4">
|
|
140
|
+
<div>
|
|
141
|
+
<div class="flex items-center gap-2 mb-1">
|
|
142
|
+
<span class="text-xs font-mono text-gray-400">${escapeHtml(knowledge.id)}</span>
|
|
143
|
+
<button type="button"
|
|
144
|
+
class="p-0.5 text-gray-400 hover:text-blue-600 rounded transition-colors"
|
|
145
|
+
title="Copy knowledge ID"
|
|
146
|
+
onclick="navigator.clipboard.writeText('${escapeHtml(knowledge.id)}').then(() => { const svg = this.querySelector('svg'); const originalPath = svg.innerHTML; svg.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>'; this.classList.add('text-green-600'); setTimeout(() => { svg.innerHTML = originalPath; this.classList.remove('text-green-600'); }, 1500); })">
|
|
147
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
148
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
|
149
|
+
</svg>
|
|
150
|
+
</button>
|
|
151
|
+
<span class="px-2 py-0.5 text-xs font-medium rounded ${knowledge.active ? 'bg-green-100 text-green-700' : 'bg-gray-200 text-gray-600'}">${knowledge.active ? 'Active' : 'Inactive'}</span>
|
|
152
|
+
${knowledge.category ? `<span class="px-2 py-0.5 text-xs font-medium rounded bg-${color}-100 text-${color}-700">${knowledge.category}</span>` : ''}
|
|
153
|
+
</div>
|
|
154
|
+
<h2 class="text-xl font-bold text-gray-800">${escapeHtml(knowledge.title)}</h2>
|
|
155
|
+
</div>
|
|
156
|
+
<button onclick="hideModal()" class="text-gray-400 hover:text-gray-600 p-1">
|
|
157
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
158
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Confidence & Usage Stats -->
|
|
164
|
+
<div class="mb-4 grid grid-cols-2 gap-4">
|
|
165
|
+
<div class="bg-gray-50 rounded-lg p-3">
|
|
166
|
+
<div class="text-xs text-gray-500 mb-1">Confidence</div>
|
|
167
|
+
<div class="text-xl font-semibold text-${color}-600 mb-2">${Math.round(knowledge.confidence * 100)}%</div>
|
|
168
|
+
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
|
169
|
+
<div class="bg-${color}-500 h-1.5 rounded-full transition-all" style="width: ${Math.round(knowledge.confidence * 100)}%"></div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="bg-gray-50 rounded-lg p-3">
|
|
173
|
+
<div class="text-xs text-gray-500 mb-1">Usage</div>
|
|
174
|
+
<div class="text-xl font-semibold text-gray-700">${knowledge.usage_count || 0} <span class="text-sm font-normal text-gray-500">times</span></div>
|
|
175
|
+
<div class="text-xs text-gray-400 mt-1">${knowledge.last_used_at ? 'Last used ' + knowledge.last_used_at.split('T')[0] : 'Never used'}</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Content -->
|
|
180
|
+
<div class="mb-4">
|
|
181
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-2">Content</h3>
|
|
182
|
+
<div class="text-sm text-gray-700 bg-gray-50 rounded-lg p-4 leading-relaxed markdown-content" data-markdown>${escapeHtml(knowledge.content)}</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Metadata -->
|
|
186
|
+
<div class="mb-4">
|
|
187
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-2">Metadata</h3>
|
|
188
|
+
<div class="text-sm text-gray-600 space-y-1">
|
|
189
|
+
<div><span class="text-gray-400">Namespace:</span> ${escapeHtml(knowledge.namespace)}</div>
|
|
190
|
+
<div><span class="text-gray-400">Scope:</span> ${knowledge.decision_scope}</div>
|
|
191
|
+
<div><span class="text-gray-400">Source:</span> ${knowledge.source}${knowledge.source === 'ticket' && knowledge.origin_ticket_type ? ` (${knowledge.origin_ticket_type})` : ''}</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
${knowledge.origin_ticket_id ? `
|
|
196
|
+
<div class="mb-4">
|
|
197
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-2">Origin Ticket</h3>
|
|
198
|
+
<span class="inline-flex items-center px-2 py-1 text-xs rounded-lg bg-yellow-50 text-yellow-700 hover:bg-yellow-100 cursor-pointer font-mono font-medium"
|
|
199
|
+
hx-get="/partials/ticket-modal/${encodeURIComponent(knowledge.origin_ticket_id)}"
|
|
200
|
+
hx-target="#modal-content"
|
|
201
|
+
hx-trigger="click">
|
|
202
|
+
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
203
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
|
204
|
+
</svg>
|
|
205
|
+
${escapeHtml(knowledge.origin_ticket_id)}
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
` : ''}
|
|
209
|
+
|
|
210
|
+
<!-- Tags -->
|
|
211
|
+
${knowledge.tags?.length ? `
|
|
212
|
+
<div class="mb-4">
|
|
213
|
+
<h3 class="text-sm font-semibold text-gray-700 mb-2">Tags</h3>
|
|
214
|
+
<div class="flex flex-wrap gap-2">
|
|
215
|
+
${knowledge.tags.map(t => `<span class="px-3 py-1 text-sm rounded-full bg-gray-100 text-gray-700">${escapeHtml(t)}</span>`).join('')}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
` : ''}
|
|
219
|
+
|
|
220
|
+
<!-- Actions & Metadata Footer -->
|
|
221
|
+
<div class="mt-6 pt-4 border-t flex items-center justify-between">
|
|
222
|
+
<div class="text-xs text-gray-400">
|
|
223
|
+
<span>Created: ${knowledge.created_at || 'N/A'}</span>
|
|
224
|
+
<span class="ml-4">Updated: ${knowledge.updated_at || 'N/A'}</span>
|
|
225
|
+
</div>
|
|
226
|
+
<button type="button"
|
|
227
|
+
class="px-3 py-1.5 text-xs font-medium ${knowledge.active ? 'text-gray-700 bg-gray-100 hover:bg-gray-200' : 'text-green-700 bg-green-50 hover:bg-green-100'} rounded transition-colors"
|
|
228
|
+
hx-patch="/api/knowledge/${knowledge.id}/active"
|
|
229
|
+
hx-vals='{"active": ${!knowledge.active}}'
|
|
230
|
+
hx-target="#modal-content"
|
|
231
|
+
hx-swap="innerHTML"
|
|
232
|
+
hx-on::after-request="htmx.trigger('#knowledge-list', 'poll-refresh')">
|
|
233
|
+
${knowledge.active ? 'Deactivate' : 'Activate'}
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getHtml(namespace: string): string;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// Main layout component for Superintent Web UI
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
// Main HTML shell with navigation, tabs, and JavaScript
|
|
4
|
+
export function getHtml(namespace) {
|
|
5
|
+
return `<!DOCTYPE html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<title>Superintent</title>
|
|
11
|
+
<link rel="stylesheet" href="/styles.css">
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@17.0.1/lib/marked.umd.js"></script>
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.1/dist/purify.min.js"></script>
|
|
15
|
+
<style>
|
|
16
|
+
.drag-over { outline: 2px dashed var(--color-blue-500); background: var(--color-blue-50); }
|
|
17
|
+
.dragging { opacity: 0.5; }
|
|
18
|
+
.htmx-indicator { display: none; }
|
|
19
|
+
.htmx-request .htmx-indicator { display: inline-block; }
|
|
20
|
+
.htmx-request.htmx-indicator { display: inline-block; }
|
|
21
|
+
.tab-active { color: var(--color-blue-700); }
|
|
22
|
+
.score-bar { height: 4px; border-radius: 2px; }
|
|
23
|
+
.modal-content { max-height: 85vh; min-height: 200px; }
|
|
24
|
+
#modal { opacity: 0; transition: opacity 150ms ease-out; }
|
|
25
|
+
#modal.show { opacity: 1; }
|
|
26
|
+
#modal-content { transform: scale(0.95); transition: transform 150ms ease-out; }
|
|
27
|
+
#modal.show #modal-content { transform: scale(1); }
|
|
28
|
+
/* Markdown prose styles */
|
|
29
|
+
.markdown-content h1, .markdown-content h2, .markdown-content h3 { font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; }
|
|
30
|
+
.markdown-content h1 { font-size: 1.25em; }
|
|
31
|
+
.markdown-content h2 { font-size: 1.1em; }
|
|
32
|
+
.markdown-content h3 { font-size: 1em; }
|
|
33
|
+
.markdown-content p { margin-bottom: 0.75em; }
|
|
34
|
+
.markdown-content ul, .markdown-content ol { margin-left: 1.5em; margin-bottom: 0.75em; }
|
|
35
|
+
.markdown-content ul { list-style-type: disc; }
|
|
36
|
+
.markdown-content ol { list-style-type: decimal; }
|
|
37
|
+
.markdown-content li { margin-bottom: 0.25em; }
|
|
38
|
+
.markdown-content code { background: var(--color-gray-200); padding: 0.125em 0.25em; border-radius: 0.25em; font-size: 0.9em; }
|
|
39
|
+
.markdown-content pre { background: var(--color-gray-800); color: var(--color-gray-100); padding: 0.75em; border-radius: 0.375em; overflow-x: auto; margin-bottom: 0.75em; }
|
|
40
|
+
.markdown-content pre code { background: none; padding: 0; color: inherit; }
|
|
41
|
+
.markdown-content strong { font-weight: 600; }
|
|
42
|
+
.markdown-content a { color: var(--color-blue-600); text-decoration: underline; }
|
|
43
|
+
.markdown-content blockquote { border-left: 3px solid var(--color-gray-300); padding-left: 1em; color: var(--color-gray-500); margin-bottom: 0.75em; }
|
|
44
|
+
.markdown-content li:has(> input[type="checkbox"]) { list-style: none; display: flex; align-items: baseline; gap: 0.4em; margin-left: -1.5em; }
|
|
45
|
+
.markdown-content li > input[type="checkbox"] { margin: 0; pointer-events: none; }
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body class="bg-gray-100 min-h-screen">
|
|
49
|
+
<!-- Navigation -->
|
|
50
|
+
<nav class="bg-white shadow-sm">
|
|
51
|
+
<div class="max-w-7xl mx-auto px-4">
|
|
52
|
+
<div class="flex items-center justify-between h-14">
|
|
53
|
+
<span class="text-sm sm:text-xl font-bold text-gray-800">Superintent</span>
|
|
54
|
+
<div class="flex">
|
|
55
|
+
<button id="tab-ticket" onclick="switchTab('ticket')"
|
|
56
|
+
class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer tab-active">
|
|
57
|
+
Ticket
|
|
58
|
+
</button>
|
|
59
|
+
<button id="tab-search" onclick="switchTab('search')"
|
|
60
|
+
class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer">
|
|
61
|
+
Search
|
|
62
|
+
</button>
|
|
63
|
+
<button id="tab-knowledge" onclick="switchTab('knowledge')"
|
|
64
|
+
class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer">
|
|
65
|
+
Knowledge
|
|
66
|
+
</button>
|
|
67
|
+
<button id="tab-spec" onclick="switchTab('spec')"
|
|
68
|
+
class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer">
|
|
69
|
+
Spec
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="text-xs text-gray-400" id="status-indicator">
|
|
73
|
+
<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-1"></span>
|
|
74
|
+
<span class="hidden sm:inline">${escapeHtml(namespace)}</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</nav>
|
|
79
|
+
|
|
80
|
+
<!-- Main Content -->
|
|
81
|
+
<main id="main-content" class="p-2 sm:p-4">
|
|
82
|
+
<!-- Ticket View (default) - full width -->
|
|
83
|
+
<div id="view-ticket" hx-get="/partials/kanban-view" hx-trigger="load"></div>
|
|
84
|
+
<!-- Search View -->
|
|
85
|
+
<div id="view-search" class="hidden max-w-7xl mx-auto" hx-get="/partials/search-view" hx-trigger="revealed"></div>
|
|
86
|
+
<!-- Knowledge View -->
|
|
87
|
+
<div id="view-knowledge" class="hidden max-w-7xl mx-auto" hx-get="/partials/knowledge-view" hx-trigger="revealed"></div>
|
|
88
|
+
<!-- Spec View -->
|
|
89
|
+
<div id="view-spec" class="hidden max-w-7xl mx-auto" hx-get="/partials/spec-view" hx-trigger="revealed"></div>
|
|
90
|
+
</main>
|
|
91
|
+
|
|
92
|
+
<!-- Modal -->
|
|
93
|
+
<div id="modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" onclick="if(event.target===this)hideModal()">
|
|
94
|
+
<div id="modal-content" class="modal-content bg-white rounded-lg shadow-2xl w-full max-w-2xl overflow-auto m-4">
|
|
95
|
+
<!-- Modal content loaded via HTMX -->
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<script>
|
|
100
|
+
// Tab switching with URL hash persistence
|
|
101
|
+
function switchTab(tab, updateHash = true) {
|
|
102
|
+
['ticket', 'search', 'knowledge', 'spec'].forEach(t => {
|
|
103
|
+
document.getElementById('view-' + t).classList.toggle('hidden', t !== tab);
|
|
104
|
+
document.getElementById('tab-' + t).classList.toggle('tab-active', t === tab);
|
|
105
|
+
});
|
|
106
|
+
// Update URL hash for refresh persistence
|
|
107
|
+
if (updateHash) {
|
|
108
|
+
history.replaceState(null, '', '#' + tab);
|
|
109
|
+
}
|
|
110
|
+
// Trigger HTMX load for lazy-loaded views
|
|
111
|
+
htmx.trigger('#view-' + tab, 'revealed');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Restore tab from URL hash on page load
|
|
115
|
+
(function() {
|
|
116
|
+
const hash = window.location.hash.slice(1);
|
|
117
|
+
if (['ticket', 'search', 'knowledge', 'spec'].includes(hash)) {
|
|
118
|
+
switchTab(hash, false);
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
|
|
122
|
+
// Markdown rendering with sanitization
|
|
123
|
+
function renderMarkdown(content) {
|
|
124
|
+
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
|
125
|
+
const html = marked.parse(content);
|
|
126
|
+
return DOMPurify.sanitize(html, {
|
|
127
|
+
ADD_TAGS: ['input'],
|
|
128
|
+
ADD_ATTR: ['type', 'checked', 'disabled'],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Fallback: escape HTML and preserve whitespace
|
|
132
|
+
return content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Render markdown content in elements with data-markdown attribute
|
|
136
|
+
function processMarkdownElements() {
|
|
137
|
+
document.querySelectorAll('[data-markdown]').forEach(function(el) {
|
|
138
|
+
if (!el.dataset.rendered) {
|
|
139
|
+
el.innerHTML = renderMarkdown(el.textContent);
|
|
140
|
+
el.dataset.rendered = 'true';
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Markdown editor: toggle between write and preview tabs
|
|
146
|
+
function initMarkdownEditor(editorId) {
|
|
147
|
+
const editor = document.getElementById(editorId);
|
|
148
|
+
if (!editor) return;
|
|
149
|
+
const textarea = editor.querySelector('textarea');
|
|
150
|
+
const preview = editor.querySelector('[data-md-preview]');
|
|
151
|
+
const writeTab = editor.querySelector('[data-md-tab="write"]');
|
|
152
|
+
const previewTab = editor.querySelector('[data-md-tab="preview"]');
|
|
153
|
+
|
|
154
|
+
writeTab.addEventListener('click', function() {
|
|
155
|
+
textarea.classList.remove('hidden');
|
|
156
|
+
preview.classList.add('hidden');
|
|
157
|
+
writeTab.classList.add('border-blue-700', 'text-blue-700');
|
|
158
|
+
writeTab.classList.remove('border-transparent', 'text-gray-500');
|
|
159
|
+
previewTab.classList.remove('border-blue-700', 'text-blue-700');
|
|
160
|
+
previewTab.classList.add('border-transparent', 'text-gray-500');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
previewTab.addEventListener('click', function() {
|
|
164
|
+
textarea.classList.add('hidden');
|
|
165
|
+
preview.classList.remove('hidden');
|
|
166
|
+
previewTab.classList.add('border-blue-700', 'text-blue-700');
|
|
167
|
+
previewTab.classList.remove('border-transparent', 'text-gray-500');
|
|
168
|
+
writeTab.classList.remove('border-blue-700', 'text-blue-700');
|
|
169
|
+
writeTab.classList.add('border-transparent', 'text-gray-500');
|
|
170
|
+
preview.innerHTML = textarea.value.trim()
|
|
171
|
+
? renderMarkdown(textarea.value)
|
|
172
|
+
: '<p class="text-gray-400 italic">Nothing to preview</p>';
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Modal functions
|
|
177
|
+
function showModal() {
|
|
178
|
+
const modal = document.getElementById('modal');
|
|
179
|
+
// Clear previous content and show loading spinner
|
|
180
|
+
document.getElementById('modal-content').innerHTML = '<div class="flex justify-center items-center py-16"><div class="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full"></div></div>';
|
|
181
|
+
modal.classList.remove('hidden');
|
|
182
|
+
document.body.style.overflow = 'hidden';
|
|
183
|
+
// Trigger reflow for transition
|
|
184
|
+
void modal.offsetWidth;
|
|
185
|
+
modal.classList.add('show');
|
|
186
|
+
}
|
|
187
|
+
function hideModal() {
|
|
188
|
+
const modal = document.getElementById('modal');
|
|
189
|
+
modal.classList.remove('show');
|
|
190
|
+
document.body.style.overflow = '';
|
|
191
|
+
// Wait for transition to finish before hiding
|
|
192
|
+
setTimeout(() => modal.classList.add('hidden'), 150);
|
|
193
|
+
}
|
|
194
|
+
document.addEventListener('keydown', (e) => {
|
|
195
|
+
if (e.key === 'Escape') hideModal();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Drag and drop
|
|
199
|
+
let draggedId = null;
|
|
200
|
+
|
|
201
|
+
function onDragStart(e, id) {
|
|
202
|
+
draggedId = id;
|
|
203
|
+
e.target.classList.add('dragging');
|
|
204
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function onDragEnd(e) {
|
|
208
|
+
e.target.classList.remove('dragging');
|
|
209
|
+
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function onDragOver(e) {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
e.currentTarget.classList.add('drag-over');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function onDragLeave(e) {
|
|
218
|
+
e.currentTarget.classList.remove('drag-over');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function onDrop(e, newStatus) {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
e.currentTarget.classList.remove('drag-over');
|
|
224
|
+
if (!draggedId) return;
|
|
225
|
+
|
|
226
|
+
fetch('/api/tickets/' + encodeURIComponent(draggedId) + '/status', {
|
|
227
|
+
method: 'PATCH',
|
|
228
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
229
|
+
body: 'status=' + encodeURIComponent(newStatus)
|
|
230
|
+
})
|
|
231
|
+
.then(r => r.json())
|
|
232
|
+
.then(data => {
|
|
233
|
+
if (data.success) {
|
|
234
|
+
// Refresh all columns
|
|
235
|
+
htmx.trigger('#kanban-columns', 'refresh');
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
draggedId = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// HTMX event handlers
|
|
242
|
+
document.body.addEventListener('htmx:afterRequest', function(e) {
|
|
243
|
+
if (e.detail.pathInfo.requestPath.includes('/status')) {
|
|
244
|
+
htmx.trigger('#kanban-columns', 'refresh');
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Process markdown elements after HTMX swaps content
|
|
249
|
+
document.body.addEventListener('htmx:afterSwap', function(e) {
|
|
250
|
+
processMarkdownElements();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Auto-refresh polling for Tickets and Knowledge tabs
|
|
254
|
+
(function() {
|
|
255
|
+
const POLL_INTERVAL = 5000; // 5 seconds
|
|
256
|
+
let pollTimer = null;
|
|
257
|
+
let currentTab = 'ticket';
|
|
258
|
+
let isPageVisible = true;
|
|
259
|
+
|
|
260
|
+
// Start polling for the current tab
|
|
261
|
+
function startPolling() {
|
|
262
|
+
stopPolling();
|
|
263
|
+
if (!isPageVisible) return;
|
|
264
|
+
|
|
265
|
+
pollTimer = setInterval(function() {
|
|
266
|
+
if (!isPageVisible) return;
|
|
267
|
+
|
|
268
|
+
if (currentTab === 'ticket') {
|
|
269
|
+
htmx.trigger('#kanban-columns', 'refresh');
|
|
270
|
+
} else if (currentTab === 'knowledge') {
|
|
271
|
+
htmx.trigger('#knowledge-list', 'poll-refresh');
|
|
272
|
+
} else if (currentTab === 'spec') {
|
|
273
|
+
htmx.trigger('#spec-list', 'poll-refresh');
|
|
274
|
+
}
|
|
275
|
+
}, POLL_INTERVAL);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Stop polling
|
|
279
|
+
function stopPolling() {
|
|
280
|
+
if (pollTimer) {
|
|
281
|
+
clearInterval(pollTimer);
|
|
282
|
+
pollTimer = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle tab switching - update currentTab and restart polling
|
|
287
|
+
const originalSwitchTab = window.switchTab;
|
|
288
|
+
window.switchTab = function(tab, updateHash) {
|
|
289
|
+
currentTab = tab;
|
|
290
|
+
originalSwitchTab(tab, updateHash);
|
|
291
|
+
// Only poll for ticket and knowledge tabs
|
|
292
|
+
if (tab === 'ticket' || tab === 'knowledge' || tab === 'spec') {
|
|
293
|
+
startPolling();
|
|
294
|
+
} else {
|
|
295
|
+
stopPolling();
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Handle page visibility changes
|
|
300
|
+
document.addEventListener('visibilitychange', function() {
|
|
301
|
+
isPageVisible = !document.hidden;
|
|
302
|
+
if (isPageVisible && (currentTab === 'ticket' || currentTab === 'knowledge' || currentTab === 'spec')) {
|
|
303
|
+
// Immediate refresh when tab becomes visible
|
|
304
|
+
if (currentTab === 'ticket') {
|
|
305
|
+
htmx.trigger('#kanban-columns', 'refresh');
|
|
306
|
+
} else if (currentTab === 'knowledge') {
|
|
307
|
+
htmx.trigger('#knowledge-list', 'poll-refresh');
|
|
308
|
+
} else if (currentTab === 'spec') {
|
|
309
|
+
htmx.trigger('#spec-list', 'poll-refresh');
|
|
310
|
+
}
|
|
311
|
+
startPolling();
|
|
312
|
+
} else {
|
|
313
|
+
stopPolling();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Initialize polling for default tab (ticket)
|
|
318
|
+
startPolling();
|
|
319
|
+
})();
|
|
320
|
+
</script>
|
|
321
|
+
</body>
|
|
322
|
+
</html>`;
|
|
323
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function renderSearchView(): string;
|
|
2
|
+
export declare function renderSearchResults(results: {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
content: string;
|
|
6
|
+
category?: string;
|
|
7
|
+
namespace: string;
|
|
8
|
+
source?: string;
|
|
9
|
+
origin_ticket_type?: string;
|
|
10
|
+
decision_scope: string;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
score: number;
|
|
13
|
+
confidence: number;
|
|
14
|
+
active: boolean;
|
|
15
|
+
}[]): string;
|