fraim 2.0.122 → 2.0.123
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 +4 -0
- package/package.json +5 -1
- 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,130 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Visa AI Hub</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='10' fill='%230d3f8a'/%3E%3Cpath d='M13 31h6l3-14h-6zm10 0h6l3-14h-6zm11-14h-6l-3 14h6z' fill='%23f7b600'/%3E%3C/svg%3E">
|
|
8
|
+
<link rel="stylesheet" href="./styles.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="app-shell">
|
|
12
|
+
<header class="app-header">
|
|
13
|
+
<div class="brand-lockup">
|
|
14
|
+
<p class="brand-title">Visa AI Hub</p>
|
|
15
|
+
<p class="brand-subtitle">Powered by <a href="https://fraimworks.ai" target="_blank" rel="noreferrer">FRAIM</a></p>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="topbar-card workspace-card">
|
|
18
|
+
<div class="workspace-section workspace-section-inline">
|
|
19
|
+
<span class="section-label">Employee</span>
|
|
20
|
+
<div class="employee-picker" id="employee-picker"></div>
|
|
21
|
+
</div>
|
|
22
|
+
<details class="project-path workspace-project">
|
|
23
|
+
<summary>
|
|
24
|
+
<span>Project Path</span>
|
|
25
|
+
<span class="project-path-preview" id="project-path-preview"></span>
|
|
26
|
+
</summary>
|
|
27
|
+
<div class="project-path-body">
|
|
28
|
+
<input id="project-path-input" class="text-input" type="text" />
|
|
29
|
+
<div class="project-actions">
|
|
30
|
+
<button id="browse-project" class="secondary-button" type="button">Choose Folder</button>
|
|
31
|
+
<button id="reload-project" class="secondary-button" type="button">Reload</button>
|
|
32
|
+
</div>
|
|
33
|
+
<p class="helper-text" id="project-status"></p>
|
|
34
|
+
</div>
|
|
35
|
+
</details>
|
|
36
|
+
</div>
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<main class="workspace">
|
|
40
|
+
<section class="workspace-intro card">
|
|
41
|
+
<div class="workspace-intro-copy">
|
|
42
|
+
<p class="section-label">Pick A Job</p>
|
|
43
|
+
<p class="panel-intro">Manage your AI employees just like you manage your teams. Tell them what jobs they need to complete, review their results, coach them, expect them to learn from you, and expect to learn from your employees.</p>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="workspace-intro-actions">
|
|
46
|
+
<p class="section-label">Job Category</p>
|
|
47
|
+
<div class="category-picker" id="category-picker"></div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section class="job-panel">
|
|
52
|
+
<div class="panel-head card">
|
|
53
|
+
<div class="panel-copy">
|
|
54
|
+
<p class="section-label">Job Category</p>
|
|
55
|
+
<h2 class="panel-title">Pick a job</h2>
|
|
56
|
+
<p class="muted">Choose the next assignment for your employee.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="job-list-card card">
|
|
61
|
+
<div class="job-list" id="job-list"></div>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
|
|
65
|
+
<section class="interaction-panel">
|
|
66
|
+
<div class="card interaction-card">
|
|
67
|
+
<div class="run-toolbar">
|
|
68
|
+
<div class="selected-job-summary">
|
|
69
|
+
<div>
|
|
70
|
+
<p class="section-label">Selected Job</p>
|
|
71
|
+
<h2 id="selected-job-title">Select a job</h2>
|
|
72
|
+
<p id="selected-job-intent" class="muted"></p>
|
|
73
|
+
</div>
|
|
74
|
+
<ul class="job-outcomes" id="selected-job-outcomes"></ul>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="run-toolbar-actions">
|
|
77
|
+
<button id="start-job" class="primary-button" type="button" disabled>Start Job</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="interaction-body">
|
|
82
|
+
<div class="conversation-column">
|
|
83
|
+
<div class="interaction-head">
|
|
84
|
+
<div>
|
|
85
|
+
<p class="section-label">Manager / Employee Interactions</p>
|
|
86
|
+
<h2 id="conversation-title">No active job</h2>
|
|
87
|
+
</div>
|
|
88
|
+
<p class="muted" id="conversation-state">Select a job and coach your employee toward your desired outcome.</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="timeline" id="timeline"></div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="manager-tools">
|
|
95
|
+
<div class="template-row">
|
|
96
|
+
<span class="section-label">Manager Templates</span>
|
|
97
|
+
<div class="template-chips" id="manager-templates"></div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<label class="composer-label" for="manager-message">Coach your employee toward your desired outcome.</label>
|
|
101
|
+
<textarea id="manager-message" class="composer" placeholder='Use FRAIM job "marketing-content-creation" and include the audience, desired outcome, and any constraints.'></textarea>
|
|
102
|
+
|
|
103
|
+
<div class="composer-actions">
|
|
104
|
+
<button id="send-coaching" class="secondary-button" type="button" disabled>Send Coaching</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<details class="card micro-manage" id="micro-manage">
|
|
111
|
+
<summary>Micro-manage</summary>
|
|
112
|
+
<div class="micro-manage-body" id="raw-history"></div>
|
|
113
|
+
</details>
|
|
114
|
+
</section>
|
|
115
|
+
</main>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<template id="timeline-message-template">
|
|
119
|
+
<article class="message">
|
|
120
|
+
<div class="message-meta">
|
|
121
|
+
<span class="message-role"></span>
|
|
122
|
+
<time class="message-time"></time>
|
|
123
|
+
</div>
|
|
124
|
+
<p class="message-text"></p>
|
|
125
|
+
</article>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<script src="./script.js"></script>
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
@@ -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();
|