fraim-framework 2.0.122 → 2.0.124
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/dist/src/ai-hub/catalog.js +131 -0
- package/dist/src/ai-hub/desktop-main.js +111 -0
- package/dist/src/ai-hub/hosts.js +241 -0
- package/dist/src/ai-hub/preferences.js +55 -0
- package/dist/src/ai-hub/server.js +307 -0
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/cli/commands/hub.js +96 -0
- package/dist/src/cli/commands/setup.js +5 -5
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/user-level-sync.js +59 -0
- package/dist/src/core/quality-evidence.js +67 -36
- package/dist/src/local-mcp-server/stdio-server.js +10 -1
- package/package.json +150 -146
- package/public/ai-hub/index.html +130 -0
- package/public/ai-hub/script.js +374 -0
- package/public/ai-hub/styles.css +568 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
bootstrap: null,
|
|
3
|
+
selectedJobId: null,
|
|
4
|
+
selectedEmployeeId: null,
|
|
5
|
+
selectedCategoryId: null,
|
|
6
|
+
activeRunId: null,
|
|
7
|
+
pollHandle: null,
|
|
8
|
+
autoMessage: '',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const elements = {
|
|
12
|
+
employeePicker: document.getElementById('employee-picker'),
|
|
13
|
+
categoryPicker: document.getElementById('category-picker'),
|
|
14
|
+
jobList: document.getElementById('job-list'),
|
|
15
|
+
selectedJobTitle: document.getElementById('selected-job-title'),
|
|
16
|
+
selectedJobIntent: document.getElementById('selected-job-intent'),
|
|
17
|
+
selectedJobOutcomes: document.getElementById('selected-job-outcomes'),
|
|
18
|
+
startJob: document.getElementById('start-job'),
|
|
19
|
+
conversationTitle: document.getElementById('conversation-title'),
|
|
20
|
+
conversationState: document.getElementById('conversation-state'),
|
|
21
|
+
timeline: document.getElementById('timeline'),
|
|
22
|
+
managerTemplates: document.getElementById('manager-templates'),
|
|
23
|
+
managerMessage: document.getElementById('manager-message'),
|
|
24
|
+
sendCoaching: document.getElementById('send-coaching'),
|
|
25
|
+
rawHistory: document.getElementById('raw-history'),
|
|
26
|
+
projectPathInput: document.getElementById('project-path-input'),
|
|
27
|
+
projectPathPreview: document.getElementById('project-path-preview'),
|
|
28
|
+
projectStatus: document.getElementById('project-status'),
|
|
29
|
+
browseProject: document.getElementById('browse-project'),
|
|
30
|
+
reloadProject: document.getElementById('reload-project'),
|
|
31
|
+
messageTemplate: document.getElementById('timeline-message-template'),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function requestJson(url, options) {
|
|
35
|
+
const response = await fetch(url, options);
|
|
36
|
+
const payload = await response.json();
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(payload.error || 'Request failed.');
|
|
39
|
+
}
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatTimestamp(value) {
|
|
44
|
+
try {
|
|
45
|
+
return new Date(value).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function humanMessageRole(role) {
|
|
52
|
+
if (role === 'manager') return 'Manager';
|
|
53
|
+
if (role === 'employee') return 'Employee';
|
|
54
|
+
return 'System';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function selectedJob() {
|
|
58
|
+
return state.bootstrap?.jobs.find((job) => job.id === state.selectedJobId) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderEmployees() {
|
|
62
|
+
elements.employeePicker.innerHTML = '';
|
|
63
|
+
for (const employee of state.bootstrap.employees) {
|
|
64
|
+
const button = document.createElement('button');
|
|
65
|
+
button.type = 'button';
|
|
66
|
+
button.className = `employee-chip${state.selectedEmployeeId === employee.id ? ' active' : ''}${employee.available ? '' : ' unavailable'}`;
|
|
67
|
+
button.textContent = employee.label;
|
|
68
|
+
button.title = employee.detail;
|
|
69
|
+
button.disabled = !employee.available;
|
|
70
|
+
button.addEventListener('click', () => {
|
|
71
|
+
state.selectedEmployeeId = employee.id;
|
|
72
|
+
render();
|
|
73
|
+
});
|
|
74
|
+
elements.employeePicker.appendChild(button);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderCategories() {
|
|
79
|
+
elements.categoryPicker.innerHTML = '';
|
|
80
|
+
for (const category of state.bootstrap.categories) {
|
|
81
|
+
const button = document.createElement('button');
|
|
82
|
+
button.type = 'button';
|
|
83
|
+
button.className = `category-chip${state.selectedCategoryId === category.id ? ' active' : ''}`;
|
|
84
|
+
button.textContent = category.label;
|
|
85
|
+
button.addEventListener('click', () => {
|
|
86
|
+
state.selectedCategoryId = category.id;
|
|
87
|
+
const firstJob = filteredJobs()[0];
|
|
88
|
+
state.selectedJobId = firstJob ? firstJob.id : null;
|
|
89
|
+
primeMessage();
|
|
90
|
+
render();
|
|
91
|
+
});
|
|
92
|
+
elements.categoryPicker.appendChild(button);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function filteredJobs() {
|
|
97
|
+
return state.bootstrap.jobs.filter((job) => job.categoryId === state.selectedCategoryId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderJobList() {
|
|
101
|
+
elements.jobList.innerHTML = '';
|
|
102
|
+
const jobs = filteredJobs();
|
|
103
|
+
if (jobs.length === 0) {
|
|
104
|
+
const empty = document.createElement('div');
|
|
105
|
+
empty.className = 'card selected-job';
|
|
106
|
+
empty.innerHTML = '<p class="muted">No FRAIM jobs were found for this category in the selected project path.</p>';
|
|
107
|
+
elements.jobList.appendChild(empty);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const job of jobs) {
|
|
112
|
+
const button = document.createElement('button');
|
|
113
|
+
button.type = 'button';
|
|
114
|
+
button.className = state.selectedJobId === job.id ? 'active' : '';
|
|
115
|
+
button.innerHTML = `<div class="job-title">${job.title}</div><div class="job-intent">${job.intent}</div>`;
|
|
116
|
+
button.addEventListener('click', () => {
|
|
117
|
+
state.selectedJobId = job.id;
|
|
118
|
+
primeMessage();
|
|
119
|
+
render();
|
|
120
|
+
});
|
|
121
|
+
elements.jobList.appendChild(button);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderSelectedJob() {
|
|
126
|
+
const job = selectedJob();
|
|
127
|
+
if (!job) {
|
|
128
|
+
elements.selectedJobTitle.textContent = 'Select a job';
|
|
129
|
+
elements.selectedJobIntent.textContent = 'Choose a Marketing or GTM job to frame the employee conversation.';
|
|
130
|
+
elements.selectedJobOutcomes.innerHTML = '';
|
|
131
|
+
elements.startJob.disabled = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
elements.selectedJobTitle.textContent = job.title;
|
|
136
|
+
elements.selectedJobIntent.textContent = job.intent;
|
|
137
|
+
elements.selectedJobOutcomes.innerHTML = '';
|
|
138
|
+
for (const outcome of job.outcome.slice(0, 3)) {
|
|
139
|
+
const item = document.createElement('li');
|
|
140
|
+
item.textContent = outcome;
|
|
141
|
+
elements.selectedJobOutcomes.appendChild(item);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
elements.startJob.disabled = !state.selectedEmployeeId || !elements.managerMessage.value.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderTemplates() {
|
|
148
|
+
elements.managerTemplates.innerHTML = '';
|
|
149
|
+
for (const template of state.bootstrap.managerTemplates) {
|
|
150
|
+
const button = document.createElement('button');
|
|
151
|
+
button.type = 'button';
|
|
152
|
+
button.className = 'template-chip';
|
|
153
|
+
button.textContent = template.title;
|
|
154
|
+
button.title = template.intent;
|
|
155
|
+
button.addEventListener('click', () => {
|
|
156
|
+
insertTemplate(template.id);
|
|
157
|
+
});
|
|
158
|
+
elements.managerTemplates.appendChild(button);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderProjectStatus() {
|
|
163
|
+
elements.projectPathInput.value = state.bootstrap.project.path;
|
|
164
|
+
const preview = state.bootstrap.project.path.split(/[\\/]/).filter(Boolean).slice(-2).join('/');
|
|
165
|
+
elements.projectPathPreview.textContent = preview || 'Choose a folder';
|
|
166
|
+
elements.projectStatus.textContent = state.bootstrap.project.message || 'FRAIM jobs are loading from this project path.';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderRun(run) {
|
|
170
|
+
elements.timeline.innerHTML = '';
|
|
171
|
+
const messages = run ? run.messages : [];
|
|
172
|
+
|
|
173
|
+
if (messages.length === 0) {
|
|
174
|
+
const empty = document.createElement('article');
|
|
175
|
+
empty.className = 'message system';
|
|
176
|
+
empty.innerHTML = '<div class="message-meta"><span class="message-role">System</span></div><p class="message-text">This conversation will show only meaningful manager and employee messages.</p>';
|
|
177
|
+
elements.timeline.appendChild(empty);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const message of messages) {
|
|
181
|
+
const fragment = elements.messageTemplate.content.cloneNode(true);
|
|
182
|
+
const article = fragment.querySelector('.message');
|
|
183
|
+
article.classList.add(message.role);
|
|
184
|
+
fragment.querySelector('.message-role').textContent = humanMessageRole(message.role);
|
|
185
|
+
fragment.querySelector('.message-time').textContent = formatTimestamp(message.createdAt);
|
|
186
|
+
fragment.querySelector('.message-text').textContent = message.text;
|
|
187
|
+
elements.timeline.appendChild(fragment);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
elements.timeline.scrollTop = elements.timeline.scrollHeight;
|
|
191
|
+
elements.rawHistory.innerHTML = '';
|
|
192
|
+
|
|
193
|
+
if (run) {
|
|
194
|
+
for (const event of run.events) {
|
|
195
|
+
const row = document.createElement('div');
|
|
196
|
+
row.className = 'micro-row';
|
|
197
|
+
row.textContent = `[${event.channel}] ${event.text}`;
|
|
198
|
+
elements.rawHistory.appendChild(row);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const job = selectedJob();
|
|
203
|
+
elements.conversationTitle.textContent = job ? job.title : 'No active job';
|
|
204
|
+
if (!run) {
|
|
205
|
+
elements.conversationState.textContent = 'Select a job and coach your employee toward your desired outcome.';
|
|
206
|
+
} else if (run.status === 'running') {
|
|
207
|
+
elements.conversationState.textContent = `${humanEmployeeLabel(run.hostId)} is working in ${run.projectPath}.`;
|
|
208
|
+
} else if (run.status === 'completed') {
|
|
209
|
+
elements.conversationState.textContent = `${humanEmployeeLabel(run.hostId)} finished the latest turn.`;
|
|
210
|
+
} else {
|
|
211
|
+
elements.conversationState.textContent = `${humanEmployeeLabel(run.hostId)} needs attention before the next turn.`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
elements.sendCoaching.disabled = !(run && run.sessionId && elements.managerMessage.value.trim());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function humanEmployeeLabel(employeeId) {
|
|
218
|
+
return state.bootstrap.employees.find((employee) => employee.id === employeeId)?.label || employeeId;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function primeMessage() {
|
|
222
|
+
const job = selectedJob();
|
|
223
|
+
if (!job) return;
|
|
224
|
+
|
|
225
|
+
const nextAutoMessage = `Use FRAIM job "${job.id}".`;
|
|
226
|
+
const current = elements.managerMessage.value.trim();
|
|
227
|
+
if (!current || current === state.autoMessage.trim()) {
|
|
228
|
+
elements.managerMessage.value = nextAutoMessage;
|
|
229
|
+
state.autoMessage = nextAutoMessage;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function insertTemplate(jobId) {
|
|
234
|
+
const fragment = `Use FRAIM job "${jobId}".`;
|
|
235
|
+
const current = elements.managerMessage.value.trim();
|
|
236
|
+
const nextValue = current ? `${current}\n${fragment}` : fragment;
|
|
237
|
+
elements.managerMessage.value = nextValue;
|
|
238
|
+
render();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function loadBootstrap(projectPath) {
|
|
242
|
+
const query = projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : '';
|
|
243
|
+
state.bootstrap = await requestJson(`/api/ai-hub/bootstrap${query}`);
|
|
244
|
+
state.selectedEmployeeId = state.selectedEmployeeId || state.bootstrap.preferences.employeeId;
|
|
245
|
+
state.selectedCategoryId = state.selectedCategoryId || state.bootstrap.preferences.categoryId;
|
|
246
|
+
|
|
247
|
+
const availableJobs = filteredJobs();
|
|
248
|
+
if (!state.selectedJobId || !availableJobs.some((job) => job.id === state.selectedJobId)) {
|
|
249
|
+
const recent = state.bootstrap.preferences.recentJobIds.find((jobId) => availableJobs.some((job) => job.id === jobId));
|
|
250
|
+
state.selectedJobId = recent || (availableJobs[0] ? availableJobs[0].id : null);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
state.activeRunId = state.bootstrap.activeRun ? state.bootstrap.activeRun.id : state.activeRunId;
|
|
254
|
+
render();
|
|
255
|
+
primeMessage();
|
|
256
|
+
render();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function render() {
|
|
260
|
+
if (!state.bootstrap) return;
|
|
261
|
+
renderEmployees();
|
|
262
|
+
renderCategories();
|
|
263
|
+
renderJobList();
|
|
264
|
+
renderSelectedJob();
|
|
265
|
+
renderTemplates();
|
|
266
|
+
renderProjectStatus();
|
|
267
|
+
|
|
268
|
+
const run = state.bootstrap.activeRun && state.activeRunId === state.bootstrap.activeRun.id
|
|
269
|
+
? state.bootstrap.activeRun
|
|
270
|
+
: null;
|
|
271
|
+
renderRun(run);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function startRun() {
|
|
275
|
+
const job = selectedJob();
|
|
276
|
+
if (!job) return;
|
|
277
|
+
|
|
278
|
+
const payload = await requestJson('/api/ai-hub/runs', {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: { 'Content-Type': 'application/json' },
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
projectPath: elements.projectPathInput.value,
|
|
283
|
+
hostId: state.selectedEmployeeId,
|
|
284
|
+
jobId: job.id,
|
|
285
|
+
message: elements.managerMessage.value,
|
|
286
|
+
}),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
state.bootstrap.activeRun = payload;
|
|
290
|
+
state.activeRunId = payload.id;
|
|
291
|
+
startPolling();
|
|
292
|
+
render();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function sendCoaching() {
|
|
296
|
+
if (!state.activeRunId) return;
|
|
297
|
+
|
|
298
|
+
const payload = await requestJson(`/api/ai-hub/runs/${state.activeRunId}/messages`, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({ message: elements.managerMessage.value }),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
state.bootstrap.activeRun = payload;
|
|
305
|
+
startPolling();
|
|
306
|
+
render();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function startPolling() {
|
|
310
|
+
if (state.pollHandle) window.clearInterval(state.pollHandle);
|
|
311
|
+
state.pollHandle = window.setInterval(async () => {
|
|
312
|
+
if (!state.activeRunId) return;
|
|
313
|
+
try {
|
|
314
|
+
const run = await requestJson(`/api/ai-hub/runs/${state.activeRunId}`);
|
|
315
|
+
state.bootstrap.activeRun = run;
|
|
316
|
+
render();
|
|
317
|
+
if (run.status !== 'running') {
|
|
318
|
+
window.clearInterval(state.pollHandle);
|
|
319
|
+
state.pollHandle = null;
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error(error);
|
|
323
|
+
}
|
|
324
|
+
}, 1000);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function chooseProjectFolder() {
|
|
328
|
+
const response = await fetch('/api/ai-hub/project-path/pick', { method: 'POST' });
|
|
329
|
+
if (response.status === 204) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const payload = await response.json();
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
throw new Error(payload.error || 'Could not choose a project folder.');
|
|
335
|
+
}
|
|
336
|
+
if (payload.path) {
|
|
337
|
+
elements.projectPathInput.value = payload.path;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
elements.startJob.addEventListener('click', async () => {
|
|
342
|
+
try {
|
|
343
|
+
await startRun();
|
|
344
|
+
} catch (error) {
|
|
345
|
+
elements.projectStatus.textContent = error.message;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
elements.sendCoaching.addEventListener('click', async () => {
|
|
350
|
+
try {
|
|
351
|
+
await sendCoaching();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
elements.projectStatus.textContent = error.message;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
elements.managerMessage.addEventListener('input', () => render());
|
|
358
|
+
elements.reloadProject.addEventListener('click', async () => {
|
|
359
|
+
try {
|
|
360
|
+
await loadBootstrap(elements.projectPathInput.value);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
elements.projectStatus.textContent = error.message;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
elements.browseProject.addEventListener('click', async () => {
|
|
367
|
+
try {
|
|
368
|
+
await chooseProjectFolder();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error(error);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
loadBootstrap();
|