fraim-framework 2.0.166 → 2.0.167
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 +20 -27
- package/dist/src/ai-hub/server.js +418 -2
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/compat.js +16 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +273 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/config-loader.js +9 -5
- package/dist/src/core/fraim-config-schema.generated.js +0 -21
- package/dist/src/core/utils/local-registry-resolver.js +8 -1
- package/package.json +5 -1
- package/public/ai-hub/index.html +81 -0
- package/public/ai-hub/review.css +13 -0
- package/public/ai-hub/script.js +414 -4
- package/public/ai-hub/styles.css +56 -0
- package/public/portfolio/ashley.html +523 -0
- package/public/portfolio/auditya.html +83 -0
- package/public/portfolio/banke.html +83 -0
- package/public/portfolio/beza.html +659 -0
- package/public/portfolio/careena.html +632 -0
- package/public/portfolio/casey.html +568 -0
- package/public/portfolio/celia.html +490 -0
- package/public/portfolio/deidre.html +642 -0
- package/public/portfolio/gautam.html +597 -0
- package/public/portfolio/hari.html +469 -0
- package/public/portfolio/huxley.html +1354 -0
- package/public/portfolio/index.html +741 -0
- package/public/portfolio/maestro.html +518 -0
- package/public/portfolio/mandy.html +590 -0
- package/public/portfolio/mona.html +597 -0
- package/public/portfolio/pam.html +887 -0
- package/public/portfolio/procella.html +107 -0
- package/public/portfolio/qasm.html +569 -0
- package/public/portfolio/ricardo.html +489 -0
- package/public/portfolio/sade.html +560 -0
- package/public/portfolio/sam.html +654 -0
- package/public/portfolio/sechar.html +580 -0
- package/public/portfolio/sreya.html +599 -0
- package/public/portfolio/swen.html +601 -0
package/public/ai-hub/script.js
CHANGED
|
@@ -756,6 +756,14 @@ function renderRail() {
|
|
|
756
756
|
titleSpan.textContent = conv.title || '';
|
|
757
757
|
bodyDiv.appendChild(titleSpan);
|
|
758
758
|
btn.appendChild(bodyDiv);
|
|
759
|
+
// Issue #566 R7: mark runs of a personalized (taught/customized) job.
|
|
760
|
+
if (isTaughtJob(conv.jobId)) {
|
|
761
|
+
const taught = document.createElement('span');
|
|
762
|
+
taught.className = 'taught-badge';
|
|
763
|
+
taught.textContent = 'Personalized';
|
|
764
|
+
taught.title = 'Personalized for this project (taught or customized in your fraim/personalized-employee layer)';
|
|
765
|
+
btn.appendChild(taught);
|
|
766
|
+
}
|
|
759
767
|
const dotClass = conversationStateDotClass(conv);
|
|
760
768
|
const statusDot = document.createElement('span');
|
|
761
769
|
statusDot.className = 'state-dot conv-state-dot dot-' + dotClass;
|
|
@@ -766,6 +774,16 @@ function renderRail() {
|
|
|
766
774
|
statusSpan.textContent = conversationStateLabel(conv);
|
|
767
775
|
statusSpan.classList.add(conversationUiState(conv));
|
|
768
776
|
btn.appendChild(statusSpan);
|
|
777
|
+
// Issue #578: sourceTrigger chip — show scheduled/webhook badge on automated runs.
|
|
778
|
+
if (conv.sourceTrigger && conv.sourceTrigger !== 'manager') {
|
|
779
|
+
const trigBadge = document.createElement('span');
|
|
780
|
+
trigBadge.className = 'trigger-badge trigger-badge--' + conv.sourceTrigger;
|
|
781
|
+
trigBadge.textContent = conv.sourceTrigger === 'scheduled' ? 'Scheduled' : 'Webhook';
|
|
782
|
+
trigBadge.title = conv.sourceTrigger === 'scheduled'
|
|
783
|
+
? 'This run was started by the built-in cron scheduler'
|
|
784
|
+
: 'This run was triggered by an inbound webhook';
|
|
785
|
+
btn.appendChild(trigBadge);
|
|
786
|
+
}
|
|
769
787
|
// Issue #442: A/B badge on rail entry.
|
|
770
788
|
if (conv.compareMode === 'ab') {
|
|
771
789
|
const badge = document.createElement('span');
|
|
@@ -1420,6 +1438,9 @@ function renderActive() {
|
|
|
1420
1438
|
// Issue #512 R7 — review experience: completion card + format artifact strip
|
|
1421
1439
|
// + unified action bar (review actions sit beside the always-on coaching chips).
|
|
1422
1440
|
renderReviewExperience(conv);
|
|
1441
|
+
// Issue #566 R4 — after the manager confirms success, offer to teach this as a
|
|
1442
|
+
// job (ad-hoc) or remember the coached steps for the job (structured).
|
|
1443
|
+
renderGrowthOffer(conv);
|
|
1423
1444
|
syncThreadMessageViewport();
|
|
1424
1445
|
const m = els['messages'];
|
|
1425
1446
|
const runningShouldStickToBottom = !!m && conv.status === 'running'
|
|
@@ -2833,6 +2854,157 @@ function renderReviewExperience(conv) {
|
|
|
2833
2854
|
}
|
|
2834
2855
|
}
|
|
2835
2856
|
|
|
2857
|
+
// ---------------------------------------------------------------------------
|
|
2858
|
+
// Issue #566 — personalize the employee: success-confirmed growth offer.
|
|
2859
|
+
// After the manager confirms a run succeeded (Approve / Mark complete / Good
|
|
2860
|
+
// job), the employee offers to make the work repeatable. Ad-hoc runs with no
|
|
2861
|
+
// matching job → "teach as a job"; structured runs the manager coached →
|
|
2862
|
+
// "remember these steps" (customize). The offer is a thin relay: accepting it
|
|
2863
|
+
// starts the evolve-employee job seeded with the run; that job owns the actual
|
|
2864
|
+
// teaching, dedup, ownership, validation, and security review.
|
|
2865
|
+
// ---------------------------------------------------------------------------
|
|
2866
|
+
|
|
2867
|
+
function jobCatalogEntry(jobId) {
|
|
2868
|
+
const jobs = (state.bootstrap && state.bootstrap.jobs) ? state.bootstrap.jobs : [];
|
|
2869
|
+
return jobs.find((j) => j.id === jobId) || null;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// A run's job is personalized when its catalog entry came from the
|
|
2873
|
+
// personalized-employee layer (R7). No author/attribution is tracked.
|
|
2874
|
+
function isTaughtJob(jobId) {
|
|
2875
|
+
const entry = jobCatalogEntry(jobId);
|
|
2876
|
+
return !!(entry && entry.personalized);
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function growthOfferHost() {
|
|
2880
|
+
const messages = els['messages'];
|
|
2881
|
+
if (!messages) return null;
|
|
2882
|
+
let host = messages.querySelector('#growth-offer');
|
|
2883
|
+
if (!host) {
|
|
2884
|
+
host = document.createElement('div');
|
|
2885
|
+
host.id = 'growth-offer';
|
|
2886
|
+
host.className = 'review-completion';
|
|
2887
|
+
}
|
|
2888
|
+
// Keep it after the latest message / completion card.
|
|
2889
|
+
messages.appendChild(host);
|
|
2890
|
+
return host;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
function clearGrowthOffer() {
|
|
2894
|
+
const messages = els['messages'];
|
|
2895
|
+
const existing = messages && messages.querySelector('#growth-offer');
|
|
2896
|
+
if (existing) existing.remove();
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
function renderGrowthOffer(conv) {
|
|
2900
|
+
// Gate on the run being successfully done (R4):
|
|
2901
|
+
// - Ad-hoc (watercooler) runs: any *completed* run qualifies. This is how an
|
|
2902
|
+
// existing watercooler conversation already sitting in the rail shows a
|
|
2903
|
+
// teach trigger when the manager opens it (the issue's headline use case) —
|
|
2904
|
+
// ad-hoc runs have no review/approve step, so "done" is the signal.
|
|
2905
|
+
// - Structured runs: require the manager's explicit approval (reviewApproved).
|
|
2906
|
+
// A structured run that merely finished is still awaiting the manager's
|
|
2907
|
+
// review, so we do not offer to customize it until they confirm it was good.
|
|
2908
|
+
// Never offer while a run is still in flight, on the teach run itself, or once
|
|
2909
|
+
// dismissed/acted on for this run.
|
|
2910
|
+
const isAdhoc = !!(conv && conv.jobId === '__freeform__');
|
|
2911
|
+
const succeeded = !!(conv && (conv.reviewApproved || (isAdhoc && conv.status === 'completed')));
|
|
2912
|
+
const isTeachRun = !!(conv && conv.jobId === 'evolve-employee');
|
|
2913
|
+
if (!succeeded || isTeachRun || conv.growthOfferDismissed || conv.growthOfferActed) {
|
|
2914
|
+
clearGrowthOffer();
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
const host = growthOfferHost();
|
|
2918
|
+
if (!host) return;
|
|
2919
|
+
const adhoc = conv.jobId === '__freeform__';
|
|
2920
|
+
host.innerHTML = '';
|
|
2921
|
+
|
|
2922
|
+
const card = document.createElement('div');
|
|
2923
|
+
card.className = 'review-card review-card--offer';
|
|
2924
|
+
card.id = 'growth-offer-card';
|
|
2925
|
+
const heading = document.createElement('div');
|
|
2926
|
+
heading.className = 'rc-heading';
|
|
2927
|
+
heading.textContent = adhoc ? '🎓 Want me to learn this?' : '🎓 Remember these steps?';
|
|
2928
|
+
card.appendChild(heading);
|
|
2929
|
+
|
|
2930
|
+
const rows = adhoc
|
|
2931
|
+
? [
|
|
2932
|
+
['What happened', 'You approved an ad-hoc task that has no matching job yet.'],
|
|
2933
|
+
['Offer', 'I can turn this into a repeatable job so you can ask for it by name next time.'],
|
|
2934
|
+
]
|
|
2935
|
+
: [
|
|
2936
|
+
['What happened', `You approved "${conv.jobTitle || conv.title || 'this run'}" and coached it along the way.`],
|
|
2937
|
+
['Offer', 'I can fold those steps into this job so I do them automatically next time.'],
|
|
2938
|
+
];
|
|
2939
|
+
for (const [k, v] of rows) {
|
|
2940
|
+
const r = document.createElement('div');
|
|
2941
|
+
r.className = 'rc-row';
|
|
2942
|
+
const ks = document.createElement('span');
|
|
2943
|
+
ks.className = 'rc-k';
|
|
2944
|
+
ks.textContent = k;
|
|
2945
|
+
const vs = document.createElement('span');
|
|
2946
|
+
vs.className = 'rc-v';
|
|
2947
|
+
vs.textContent = v;
|
|
2948
|
+
r.appendChild(ks);
|
|
2949
|
+
r.appendChild(vs);
|
|
2950
|
+
card.appendChild(r);
|
|
2951
|
+
}
|
|
2952
|
+
host.appendChild(card);
|
|
2953
|
+
|
|
2954
|
+
const strip = document.createElement('div');
|
|
2955
|
+
strip.className = 'rc-artifact-strip';
|
|
2956
|
+
const teachBtn = document.createElement('button');
|
|
2957
|
+
teachBtn.type = 'button';
|
|
2958
|
+
teachBtn.className = 'rc-art-btn rc-art-btn--teach';
|
|
2959
|
+
teachBtn.id = 'growth-teach-btn';
|
|
2960
|
+
teachBtn.textContent = adhoc ? '🎓 Teach as a job' : '✨ Remember for this job';
|
|
2961
|
+
teachBtn.addEventListener('click', () => {
|
|
2962
|
+
conv.growthOfferActed = true;
|
|
2963
|
+
upsertConversation(conv);
|
|
2964
|
+
startTeachFlow({ seedConv: conv, mode: adhoc ? 'teach' : 'customize', seedSource: 'run' });
|
|
2965
|
+
});
|
|
2966
|
+
const dismissBtn = document.createElement('button');
|
|
2967
|
+
dismissBtn.type = 'button';
|
|
2968
|
+
dismissBtn.className = 'rc-art-btn ghost';
|
|
2969
|
+
dismissBtn.id = 'growth-dismiss-btn';
|
|
2970
|
+
dismissBtn.textContent = 'Not now';
|
|
2971
|
+
dismissBtn.addEventListener('click', () => {
|
|
2972
|
+
conv.growthOfferDismissed = true;
|
|
2973
|
+
upsertConversation(conv);
|
|
2974
|
+
renderActive();
|
|
2975
|
+
});
|
|
2976
|
+
strip.appendChild(teachBtn);
|
|
2977
|
+
strip.appendChild(dismissBtn);
|
|
2978
|
+
host.appendChild(strip);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// Start the evolve-employee job, optionally seeded from a completed run or a
|
|
2982
|
+
// document. This is the single entry point shared by the success-confirmed
|
|
2983
|
+
// offer and the "+ New job" palette teach entries (R1/R4).
|
|
2984
|
+
function startTeachFlow(opts) {
|
|
2985
|
+
const options = opts || {};
|
|
2986
|
+
const employeeId = (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.employeeId) || 'claude';
|
|
2987
|
+
const seedConv = options.seedConv || null;
|
|
2988
|
+
let instructions = '';
|
|
2989
|
+
if (options.seedSource === 'run' && seedConv) {
|
|
2990
|
+
const transcript = (seedConv.messages || [])
|
|
2991
|
+
.map((m) => `${m.role}: ${typeof m.text === 'string' ? m.text : ''}`)
|
|
2992
|
+
.join('\n')
|
|
2993
|
+
.slice(0, 4000);
|
|
2994
|
+
if (options.mode === 'customize') {
|
|
2995
|
+
instructions = `Customize the "${seedConv.jobTitle || seedConv.jobId}" job. I coached this run with steps you should remember next time. Use this run as the seed: pre-fill the intake from the coaching turns and confirm with me before changing anything. Keep the same owning employee and preserve the existing phases.\n\nRun: ${seedConv.title || ''}\n\nTranscript:\n${transcript}`;
|
|
2996
|
+
} else {
|
|
2997
|
+
instructions = `Teach yourself a new repeatable job from this successful run. Use the run as the seed: restate the ask, the steps you performed, and the deliverable; propose a job name; check for an existing job first; and recommend which employee should own it.\n\nRun: ${seedConv.title || ''}\n\nTranscript:\n${transcript}`;
|
|
2998
|
+
}
|
|
2999
|
+
} else if (options.seedSource === 'document') {
|
|
3000
|
+
instructions = 'Teach yourself a new job from an SOP or document. Ask me for the document, then map its sections and steps onto job phases and confirm the mapping with me before drafting. Recommend which employee should own it.';
|
|
3001
|
+
} else {
|
|
3002
|
+
instructions = 'Teach yourself a new job. I will describe the capability I want. Pre-fill the plan, check for an existing job first, and recommend which employee should own it.';
|
|
3003
|
+
}
|
|
3004
|
+
const job = { id: 'evolve-employee', title: options.mode === 'customize' ? 'Improve a job' : 'Teach a new skill' };
|
|
3005
|
+
startRun(job, instructions, employeeId);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
2836
3008
|
// Comments follow the artifact (R7.7): a PR opens GitHub; local artifacts open
|
|
2837
3009
|
// from disk; reports view inline. Nothing new is invented - these surface what
|
|
2838
3010
|
// the read-side already knows and otherwise leave a status hint.
|
|
@@ -3188,10 +3360,28 @@ function renderCpRows(searchText) {
|
|
|
3188
3360
|
}
|
|
3189
3361
|
}
|
|
3190
3362
|
|
|
3191
|
-
//
|
|
3363
|
+
// Issue #566 R4: teach entries start the evolve-employee job. They appear at
|
|
3364
|
+
// the top of the catalog, only when no persona filter is active (teaching is
|
|
3365
|
+
// not persona-scoped), and match a search over their labels.
|
|
3366
|
+
const teachRows = [];
|
|
3367
|
+
if (personaFilter === null) {
|
|
3368
|
+
if (!q || 'teach a new skill'.includes(q)) {
|
|
3369
|
+
teachRows.push({ type: 'teach', teachSeed: 'description', job: { id: 'evolve-employee', title: 'Teach a new skill', intent: 'Teach me a new job in plain language.' } });
|
|
3370
|
+
}
|
|
3371
|
+
if (!q || 'teach from a document'.includes(q) || 'sop'.includes(q)) {
|
|
3372
|
+
teachRows.push({ type: 'teach', teachSeed: 'document', job: { id: 'evolve-employee', title: 'Teach from a document', intent: 'Point me at an SOP or document and I will turn it into a phased job.' } });
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// Build flat row list: recent first, then catalog jobs, then teach entries
|
|
3377
|
+
// last. Teach entries go after the catalog jobs so the first runnable job
|
|
3378
|
+
// stays the default keyboard selection (ArrowDown+Enter runs a job, not a
|
|
3379
|
+
// teach flow). The flat order must match the render order below so
|
|
3380
|
+
// click/keyboard indices line up.
|
|
3192
3381
|
state.cpRows = [
|
|
3193
3382
|
...recentRows,
|
|
3194
3383
|
...catalogJobs.map((j) => ({ type: 'job', job: j, instructions: '' })),
|
|
3384
|
+
...teachRows,
|
|
3195
3385
|
];
|
|
3196
3386
|
state.cpHighlightIndex = -1;
|
|
3197
3387
|
|
|
@@ -3206,13 +3396,17 @@ function renderCpRows(searchText) {
|
|
|
3206
3396
|
});
|
|
3207
3397
|
}
|
|
3208
3398
|
|
|
3209
|
-
// Render catalog section
|
|
3399
|
+
// Render catalog section: catalog jobs first, then teach entries last.
|
|
3400
|
+
// flatIndex continues from recentRows so it matches state.cpRows order.
|
|
3210
3401
|
const catalogList = document.getElementById('cp-catalog-list');
|
|
3211
3402
|
if (catalogList) {
|
|
3212
3403
|
catalogList.innerHTML = '';
|
|
3213
3404
|
catalogJobs.forEach((job, i) => {
|
|
3214
3405
|
catalogList.appendChild(buildCpRow({ type: 'job', job, instructions: '' }, recentRows.length + i));
|
|
3215
3406
|
});
|
|
3407
|
+
teachRows.forEach((row, i) => {
|
|
3408
|
+
catalogList.appendChild(buildCpRow(row, recentRows.length + catalogJobs.length + i));
|
|
3409
|
+
});
|
|
3216
3410
|
}
|
|
3217
3411
|
|
|
3218
3412
|
// Update employee footer
|
|
@@ -3233,7 +3427,7 @@ function buildCpRow(row, flatIndex) {
|
|
|
3233
3427
|
|
|
3234
3428
|
const icon = document.createElement('span');
|
|
3235
3429
|
icon.className = 'cp-row-icon';
|
|
3236
|
-
icon.textContent = row.type === 'recent' ? '🕐' : '📋';
|
|
3430
|
+
icon.textContent = row.type === 'recent' ? '🕐' : row.type === 'teach' ? '🎓' : '📋';
|
|
3237
3431
|
|
|
3238
3432
|
const body = document.createElement('span');
|
|
3239
3433
|
body.className = 'cp-row-body';
|
|
@@ -3305,6 +3499,12 @@ function selectCpRow(index) {
|
|
|
3305
3499
|
|
|
3306
3500
|
if (row.type === 'recent' && row.instructions) {
|
|
3307
3501
|
instr.value = row.instructions;
|
|
3502
|
+
} else if (row.type === 'teach') {
|
|
3503
|
+
// Issue #566: seed the instruction so the manager confirms rather than
|
|
3504
|
+
// re-authoring. The evolve-employee job reads the seed source from this.
|
|
3505
|
+
instr.value = row.teachSeed === 'document'
|
|
3506
|
+
? 'Teach yourself a new job from this document: '
|
|
3507
|
+
: 'Teach yourself a new job: ';
|
|
3308
3508
|
} else {
|
|
3309
3509
|
instr.value = '';
|
|
3310
3510
|
}
|
|
@@ -4055,7 +4255,18 @@ function startPolling() {
|
|
|
4055
4255
|
}
|
|
4056
4256
|
try {
|
|
4057
4257
|
if (!fraimDone) {
|
|
4058
|
-
const
|
|
4258
|
+
const runResp = await fetch(`/api/ai-hub/runs/${conv.runId}`, { headers: { 'x-api-key': 'local-dev' } });
|
|
4259
|
+
if (runResp.status === 404) {
|
|
4260
|
+
// Run no longer in server registry (e.g. after restart). Stop polling.
|
|
4261
|
+
window.clearInterval(state.pollHandle);
|
|
4262
|
+
state.pollHandle = null;
|
|
4263
|
+
conv.status = 'done';
|
|
4264
|
+
upsertConversation(conv);
|
|
4265
|
+
renderRail();
|
|
4266
|
+
renderActive();
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
const run = await runResp.json();
|
|
4059
4270
|
foldRunIntoConversation(conv, run);
|
|
4060
4271
|
}
|
|
4061
4272
|
// Issue #442: also poll the compare run when present and not yet terminal.
|
|
@@ -5161,6 +5372,8 @@ function tfSelectProjectView(view, projectId) {
|
|
|
5161
5372
|
tfRenderTree();
|
|
5162
5373
|
tfRenderProjectContextTop();
|
|
5163
5374
|
tfApplyWorkspaceMode();
|
|
5375
|
+
// Issue #578: reload deployment roster when entering workspace.
|
|
5376
|
+
loadDeployments();
|
|
5164
5377
|
}
|
|
5165
5378
|
}
|
|
5166
5379
|
|
|
@@ -5642,6 +5855,197 @@ function tfShowProjectBrief() { const a = document.getElementById('proj-brief-ac
|
|
|
5642
5855
|
function tfShowProjectLearnings() { const a = document.getElementById('proj-learnings-acc'); if (a) a.open = true; }
|
|
5643
5856
|
function tfHideProjectLearnings() { /* panels removed in #521; brief/learnings are inline top sections */ }
|
|
5644
5857
|
|
|
5858
|
+
// ---------------------------------------------------------------------------
|
|
5859
|
+
// Issue #578: Deployment roster (scheduled + webhook triggers)
|
|
5860
|
+
// ---------------------------------------------------------------------------
|
|
5861
|
+
|
|
5862
|
+
async function loadDeployments() {
|
|
5863
|
+
const list = document.getElementById('proj-deployments-list');
|
|
5864
|
+
if (!list) return;
|
|
5865
|
+
try {
|
|
5866
|
+
const [schResp, whResp] = await Promise.all([
|
|
5867
|
+
fetch('/api/ai-hub/schedules'),
|
|
5868
|
+
fetch('/api/ai-hub/webhooks'),
|
|
5869
|
+
]);
|
|
5870
|
+
const schedules = schResp.ok ? await schResp.json() : [];
|
|
5871
|
+
const webhooks = whResp.ok ? await whResp.json() : [];
|
|
5872
|
+
renderDeploymentList(list, [...schedules, ...webhooks]);
|
|
5873
|
+
} catch {
|
|
5874
|
+
list.textContent = 'Could not load deployments.';
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
function renderDeploymentList(container, deployments) {
|
|
5879
|
+
container.innerHTML = '';
|
|
5880
|
+
if (!deployments.length) {
|
|
5881
|
+
const empty = document.createElement('p');
|
|
5882
|
+
empty.className = 'dep-empty';
|
|
5883
|
+
empty.textContent = 'No deployments yet. Use + Schedule or + Webhook above to add automated triggers.';
|
|
5884
|
+
container.appendChild(empty);
|
|
5885
|
+
return;
|
|
5886
|
+
}
|
|
5887
|
+
for (const dep of deployments) {
|
|
5888
|
+
const card = document.createElement('div');
|
|
5889
|
+
card.className = 'dep-card';
|
|
5890
|
+
const top = document.createElement('div');
|
|
5891
|
+
top.className = 'dep-card-top';
|
|
5892
|
+
const icon = document.createElement('span');
|
|
5893
|
+
icon.className = 'dep-type-icon';
|
|
5894
|
+
icon.textContent = dep.type === 'scheduled' ? '⏱' : '🔗';
|
|
5895
|
+
const label = document.createElement('span');
|
|
5896
|
+
label.className = 'dep-label';
|
|
5897
|
+
label.textContent = dep.label;
|
|
5898
|
+
const typeBadge = document.createElement('span');
|
|
5899
|
+
typeBadge.className = 'dep-type-badge dep-type-badge--' + dep.type;
|
|
5900
|
+
typeBadge.textContent = dep.type === 'scheduled' ? 'Scheduled' : 'Webhook';
|
|
5901
|
+
top.appendChild(icon);
|
|
5902
|
+
top.appendChild(label);
|
|
5903
|
+
top.appendChild(typeBadge);
|
|
5904
|
+
card.appendChild(top);
|
|
5905
|
+
|
|
5906
|
+
if (dep.cronExpr) {
|
|
5907
|
+
const cron = document.createElement('div');
|
|
5908
|
+
cron.className = 'dep-detail';
|
|
5909
|
+
cron.textContent = dep.cronExpr;
|
|
5910
|
+
card.appendChild(cron);
|
|
5911
|
+
}
|
|
5912
|
+
if (dep.inboundUrl) {
|
|
5913
|
+
const urlRow = document.createElement('div');
|
|
5914
|
+
urlRow.className = 'dep-detail dep-detail--url';
|
|
5915
|
+
const urlCode = document.createElement('code');
|
|
5916
|
+
urlCode.textContent = dep.inboundUrl;
|
|
5917
|
+
urlCode.className = 'dep-inbound-url dep-inbound-url--inline';
|
|
5918
|
+
const copyBtn = document.createElement('button');
|
|
5919
|
+
copyBtn.type = 'button';
|
|
5920
|
+
copyBtn.className = 'hm-copy-btn';
|
|
5921
|
+
copyBtn.textContent = 'Copy';
|
|
5922
|
+
copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(dep.inboundUrl).catch(() => {}); });
|
|
5923
|
+
urlRow.appendChild(urlCode);
|
|
5924
|
+
urlRow.appendChild(copyBtn);
|
|
5925
|
+
card.appendChild(urlRow);
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
const delBtn = document.createElement('button');
|
|
5929
|
+
delBtn.type = 'button';
|
|
5930
|
+
delBtn.className = 'dep-del-btn';
|
|
5931
|
+
delBtn.title = 'Remove this deployment';
|
|
5932
|
+
delBtn.textContent = 'Remove';
|
|
5933
|
+
delBtn.addEventListener('click', async () => {
|
|
5934
|
+
const endpoint = dep.type === 'scheduled' ? 'schedules' : 'webhooks';
|
|
5935
|
+
await fetch('/api/ai-hub/' + endpoint + '/' + dep.id, { method: 'DELETE' });
|
|
5936
|
+
await loadDeployments();
|
|
5937
|
+
});
|
|
5938
|
+
card.appendChild(delBtn);
|
|
5939
|
+
container.appendChild(card);
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
|
|
5943
|
+
function initDeploymentButtons() {
|
|
5944
|
+
const addSchBtn = document.getElementById('dep-add-schedule-btn');
|
|
5945
|
+
const addWhBtn = document.getElementById('dep-add-webhook-btn');
|
|
5946
|
+
if (addSchBtn) addSchBtn.addEventListener('click', () => openDeploymentModal('schedule'));
|
|
5947
|
+
if (addWhBtn) addWhBtn.addEventListener('click', () => openDeploymentModal('webhook'));
|
|
5948
|
+
|
|
5949
|
+
// Close buttons
|
|
5950
|
+
const schClose = document.getElementById('dep-schedule-close');
|
|
5951
|
+
const whClose = document.getElementById('dep-webhook-close');
|
|
5952
|
+
if (schClose) schClose.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
|
|
5953
|
+
if (whClose) whClose.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
|
|
5954
|
+
|
|
5955
|
+
// Cancel buttons
|
|
5956
|
+
const schCancel = document.getElementById('dep-sch-cancel-btn');
|
|
5957
|
+
const whCancel = document.getElementById('dep-wh-cancel-btn');
|
|
5958
|
+
if (schCancel) schCancel.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
|
|
5959
|
+
if (whCancel) whCancel.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
|
|
5960
|
+
|
|
5961
|
+
// Save buttons
|
|
5962
|
+
const schSave = document.getElementById('dep-sch-save-btn');
|
|
5963
|
+
if (schSave) schSave.addEventListener('click', saveScheduleDeployment);
|
|
5964
|
+
const whSave = document.getElementById('dep-wh-save-btn');
|
|
5965
|
+
if (whSave) whSave.addEventListener('click', saveWebhookDeployment);
|
|
5966
|
+
|
|
5967
|
+
// Copy inbound URL button
|
|
5968
|
+
const copyBtn = document.getElementById('dep-wh-copy-btn');
|
|
5969
|
+
if (copyBtn) copyBtn.addEventListener('click', () => {
|
|
5970
|
+
const url = document.getElementById('dep-wh-inbound-url').textContent;
|
|
5971
|
+
navigator.clipboard.writeText(url).catch(() => {});
|
|
5972
|
+
});
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5975
|
+
function openDeploymentModal(type) {
|
|
5976
|
+
const jobs = state.bootstrap?.jobs ?? [];
|
|
5977
|
+
if (type === 'schedule') {
|
|
5978
|
+
const modal = document.getElementById('dep-schedule-modal');
|
|
5979
|
+
const jobSel = document.getElementById('dep-sch-job');
|
|
5980
|
+
jobSel.innerHTML = '';
|
|
5981
|
+
for (const j of jobs) {
|
|
5982
|
+
const opt = document.createElement('option');
|
|
5983
|
+
opt.value = j.id;
|
|
5984
|
+
opt.textContent = j.title;
|
|
5985
|
+
jobSel.appendChild(opt);
|
|
5986
|
+
}
|
|
5987
|
+
document.getElementById('dep-sch-error').hidden = true;
|
|
5988
|
+
document.getElementById('dep-sch-label').value = '';
|
|
5989
|
+
document.getElementById('dep-sch-cron').value = '';
|
|
5990
|
+
document.getElementById('dep-sch-instructions').value = '';
|
|
5991
|
+
modal.hidden = false;
|
|
5992
|
+
} else {
|
|
5993
|
+
const modal = document.getElementById('dep-webhook-modal');
|
|
5994
|
+
const jobSel = document.getElementById('dep-wh-job');
|
|
5995
|
+
jobSel.innerHTML = '';
|
|
5996
|
+
for (const j of jobs) {
|
|
5997
|
+
const opt = document.createElement('option');
|
|
5998
|
+
opt.value = j.id;
|
|
5999
|
+
opt.textContent = j.title;
|
|
6000
|
+
jobSel.appendChild(opt);
|
|
6001
|
+
}
|
|
6002
|
+
document.getElementById('dep-wh-error').hidden = true;
|
|
6003
|
+
document.getElementById('dep-wh-inbound-row').hidden = true;
|
|
6004
|
+
document.getElementById('dep-wh-label').value = '';
|
|
6005
|
+
document.getElementById('dep-wh-instructions').value = '';
|
|
6006
|
+
modal.hidden = false;
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
async function saveScheduleDeployment() {
|
|
6011
|
+
const label = document.getElementById('dep-sch-label').value.trim();
|
|
6012
|
+
const jobId = document.getElementById('dep-sch-job').value;
|
|
6013
|
+
const cronExpr = document.getElementById('dep-sch-cron').value.trim();
|
|
6014
|
+
const instructions = document.getElementById('dep-sch-instructions').value.trim();
|
|
6015
|
+
const errEl = document.getElementById('dep-sch-error');
|
|
6016
|
+
if (!label || !cronExpr) { errEl.textContent = 'Label and cron expression are required.'; errEl.hidden = false; return; }
|
|
6017
|
+
try {
|
|
6018
|
+
const resp = await fetch('/api/ai-hub/schedules', {
|
|
6019
|
+
method: 'POST',
|
|
6020
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6021
|
+
body: JSON.stringify({ label, jobId, cronExpr, instructions: instructions || undefined }),
|
|
6022
|
+
});
|
|
6023
|
+
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to create schedule.'; errEl.hidden = false; return; }
|
|
6024
|
+
document.getElementById('dep-schedule-modal').hidden = true;
|
|
6025
|
+
await loadDeployments();
|
|
6026
|
+
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
async function saveWebhookDeployment() {
|
|
6030
|
+
const label = document.getElementById('dep-wh-label').value.trim();
|
|
6031
|
+
const jobId = document.getElementById('dep-wh-job').value;
|
|
6032
|
+
const instructions = document.getElementById('dep-wh-instructions').value.trim();
|
|
6033
|
+
const errEl = document.getElementById('dep-wh-error');
|
|
6034
|
+
if (!label) { errEl.textContent = 'Label is required.'; errEl.hidden = false; return; }
|
|
6035
|
+
try {
|
|
6036
|
+
const resp = await fetch('/api/ai-hub/webhooks', {
|
|
6037
|
+
method: 'POST',
|
|
6038
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6039
|
+
body: JSON.stringify({ label, jobId, instructions: instructions || undefined }),
|
|
6040
|
+
});
|
|
6041
|
+
if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to create webhook.'; errEl.hidden = false; return; }
|
|
6042
|
+
const dep = await resp.json();
|
|
6043
|
+
document.getElementById('dep-wh-inbound-url').textContent = dep.inboundUrl;
|
|
6044
|
+
document.getElementById('dep-wh-inbound-row').hidden = false;
|
|
6045
|
+
await loadDeployments();
|
|
6046
|
+
} catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
|
|
6047
|
+
}
|
|
6048
|
+
|
|
5645
6049
|
// ---------------------------------------------------------------------------
|
|
5646
6050
|
// Company / Manager / Brain content
|
|
5647
6051
|
// ---------------------------------------------------------------------------
|
|
@@ -7615,6 +8019,8 @@ function tfInitShell() {
|
|
|
7615
8019
|
tfRenderRail();
|
|
7616
8020
|
tfRenderProjectTabs();
|
|
7617
8021
|
tfShowArea('projects');
|
|
8022
|
+
// Issue #578: wire deployment modal buttons.
|
|
8023
|
+
initDeploymentButtons();
|
|
7618
8024
|
// Open the workspace by default so the conversation surface (#conversation,
|
|
7619
8025
|
// #new-conv-btn, #empty, the welcome line, the project button) is visible on
|
|
7620
8026
|
// load — preserving the contract the shipped #339/#347/#429/#442 Hub UI
|
|
@@ -7668,4 +8074,8 @@ if (typeof window !== 'undefined') {
|
|
|
7668
8074
|
window.persistConversations = persistConversations;
|
|
7669
8075
|
window.renderRail = renderRail;
|
|
7670
8076
|
window.renderActive = renderActive;
|
|
8077
|
+
// Issue #566: let the suite open a conversation and read the active one so it
|
|
8078
|
+
// can exercise the success-confirmed growth offer deterministically.
|
|
8079
|
+
window.switchToConversation = switchToConversation;
|
|
8080
|
+
window.activeConversation = activeConversation;
|
|
7671
8081
|
}
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
--shadow-lg: 0 0 0 1px rgba(0, 0, 0, 0.10), 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
27
27
|
--picker-delegation: #5b7eb0;
|
|
28
28
|
--picker-learning: #8b5fbf;
|
|
29
|
+
/* Issue #566: personalization / "teach" accent (growth offer + taught badge). */
|
|
30
|
+
--teach: #5b3fa8;
|
|
31
|
+
--teach-soft: #f6f2ff;
|
|
32
|
+
--teach-line: #d8cdf2;
|
|
29
33
|
/* Safe zone for macOS hiddenInset traffic lights (~28px) and
|
|
30
34
|
Windows titleBarOverlay height (36px). Falls back to 0 in a browser tab. */
|
|
31
35
|
--titlebar-inset: env(titlebar-area-height, 0px);
|
|
@@ -2047,6 +2051,22 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
2047
2051
|
flex-shrink: 0;
|
|
2048
2052
|
}
|
|
2049
2053
|
|
|
2054
|
+
/* Issue #566 R7: "Taught by you" marking on runs of a personalized job.
|
|
2055
|
+
Follows the .ab-badge inline-badge pattern with a distinct purple accent. */
|
|
2056
|
+
.taught-badge {
|
|
2057
|
+
margin-left: auto;
|
|
2058
|
+
padding: 1px 6px;
|
|
2059
|
+
font-size: 9.5px;
|
|
2060
|
+
font-weight: 700;
|
|
2061
|
+
letter-spacing: 0.03em;
|
|
2062
|
+
border-radius: 999px;
|
|
2063
|
+
background: var(--teach-soft);
|
|
2064
|
+
color: var(--teach);
|
|
2065
|
+
border: 1px solid var(--teach-line);
|
|
2066
|
+
white-space: nowrap;
|
|
2067
|
+
flex-shrink: 0;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2050
2070
|
/* Full-panel A/B split: #conversation becomes a flex row */
|
|
2051
2071
|
#conversation.ab-mode {
|
|
2052
2072
|
display: flex;
|
|
@@ -3965,3 +3985,39 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
3965
3985
|
.hs-btn--primary:hover { opacity:.85; }
|
|
3966
3986
|
.hs-btn--ghost { background:transparent; color:var(--muted); border:1px solid var(--line); }
|
|
3967
3987
|
.hs-btn--ghost:hover { color:var(--text); border-color:var(--text); }
|
|
3988
|
+
|
|
3989
|
+
/* ── Issue #578: Scheduled + Reactive Employees ── */
|
|
3990
|
+
|
|
3991
|
+
/* sourceTrigger chip on run rail items */
|
|
3992
|
+
.trigger-badge { display:inline-block; padding:1px 6px; border-radius:4px; font-size:10px; font-weight:600; letter-spacing:.02em; flex-shrink:0; }
|
|
3993
|
+
.trigger-badge--scheduled { background:rgba(0,122,255,.12); color:#0071e3; }
|
|
3994
|
+
.trigger-badge--webhook { background:rgba(88,86,214,.12); color:#5856d6; }
|
|
3995
|
+
|
|
3996
|
+
/* Deployment roster accordion */
|
|
3997
|
+
#proj-deployments-acc { margin-top:0; }
|
|
3998
|
+
.dep-actions { display:flex; gap:8px; margin-top:8px; }
|
|
3999
|
+
.dep-add-btn { padding:4px 12px; background:var(--accent-soft,rgba(0,113,227,.08)); color:var(--accent,#0071e3); border:1px solid rgba(0,113,227,.2); border-radius:8px; font-size:12px; font-weight:600; cursor:pointer; }
|
|
4000
|
+
.dep-add-btn:hover { background:var(--accent,#0071e3); color:#fff; }
|
|
4001
|
+
|
|
4002
|
+
/* Deployment cards */
|
|
4003
|
+
.dep-card { display:flex; flex-wrap:wrap; align-items:baseline; gap:6px; padding:8px 10px; background:var(--surface,#fff); border:1px solid var(--line,#e5e5e5); border-radius:10px; margin-bottom:6px; }
|
|
4004
|
+
.dep-card-top { display:flex; align-items:center; gap:6px; flex:1; min-width:0; }
|
|
4005
|
+
.dep-type-icon { font-size:14px; flex-shrink:0; }
|
|
4006
|
+
.dep-label { font-size:13px; font-weight:600; color:var(--text,#1d1d1f); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
4007
|
+
.dep-type-badge { display:inline-block; padding:1px 7px; border-radius:4px; font-size:10px; font-weight:600; flex-shrink:0; }
|
|
4008
|
+
.dep-type-badge--scheduled { background:rgba(0,122,255,.12); color:#0071e3; }
|
|
4009
|
+
.dep-type-badge--webhook { background:rgba(88,86,214,.12); color:#5856d6; }
|
|
4010
|
+
.dep-detail { width:100%; font-size:11px; color:var(--muted,#888); margin-left:20px; }
|
|
4011
|
+
.dep-detail--url { display:flex; align-items:center; gap:6px; }
|
|
4012
|
+
.dep-inbound-url { font-size:11px; word-break:break-all; color:var(--muted,#888); flex:1; }
|
|
4013
|
+
.dep-inbound-url--inline { display:inline; font-size:11px; }
|
|
4014
|
+
.dep-del-btn { margin-left:auto; padding:2px 8px; background:transparent; border:1px solid var(--line,#e5e5e5); border-radius:6px; font-size:11px; color:var(--muted,#888); cursor:pointer; flex-shrink:0; }
|
|
4015
|
+
.dep-del-btn:hover { border-color:#ff3b30; color:#ff3b30; }
|
|
4016
|
+
.dep-empty { color:var(--muted,#888); font-size:12px; margin:4px 0 8px; line-height:1.5; }
|
|
4017
|
+
|
|
4018
|
+
/* Deployment modals */
|
|
4019
|
+
.dep-modal-actions { display:flex; gap:8px; margin-top:12px; }
|
|
4020
|
+
.dep-error { color:#ff3b30; font-size:12px; margin-top:4px; }
|
|
4021
|
+
.cancel-button { padding:7px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; background:transparent; color:var(--muted,#888); border:1px solid var(--line,#e5e5e5); }
|
|
4022
|
+
.cancel-button:hover { border-color:var(--text,#1d1d1f); color:var(--text,#1d1d1f); }
|
|
4023
|
+
.dep-inbound-url-row { display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--bg,#f5f5f7); border-radius:8px; margin-top:6px; }
|