fraim-hub 2.0.261 → 2.0.262
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.
|
@@ -7,6 +7,7 @@ exports.packsDir = packsDir;
|
|
|
7
7
|
exports.defaultCloneDir = defaultCloneDir;
|
|
8
8
|
exports.resolvePackHome = resolvePackHome;
|
|
9
9
|
exports.packReadRoots = packReadRoots;
|
|
10
|
+
exports.migrateStrandedContent = migrateStrandedContent;
|
|
10
11
|
exports.findStrandedLegacyContent = findStrandedLegacyContent;
|
|
11
12
|
exports.gitUrlHasUserinfo = gitUrlHasUserinfo;
|
|
12
13
|
exports.ensurePackClone = ensurePackClone;
|
|
@@ -172,6 +173,78 @@ function resolvePackHome(layer) {
|
|
|
172
173
|
function packReadRoots(layer) {
|
|
173
174
|
return [resolvePackHome(layer).contentRoot];
|
|
174
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Auto-migrate content stranded at the legacy standard path to the configured
|
|
178
|
+
* contentRoot. Runs at most once per process per (layer + contentRoot) pair.
|
|
179
|
+
* Never throws: migration failures are silently skipped.
|
|
180
|
+
*
|
|
181
|
+
* Applies when a synced backend (local-folder, git, fraim-cloud) is configured
|
|
182
|
+
* and the legacy standard path differs from contentRoot. Each file is moved
|
|
183
|
+
* with renameSync; on a cross-device rename (e.g. ~/.fraim → OneDrive) it
|
|
184
|
+
* falls back to copyFileSync + unlinkSync. Files already present in contentRoot
|
|
185
|
+
* are never overwritten — contentRoot is always authoritative.
|
|
186
|
+
*/
|
|
187
|
+
const _migratedStrandedLayers = new Set();
|
|
188
|
+
function migrateStrandedContent(layer) {
|
|
189
|
+
const home = resolvePackHome(layer);
|
|
190
|
+
const key = `${layer}:${home.contentRoot}`;
|
|
191
|
+
if (_migratedStrandedLayers.has(key))
|
|
192
|
+
return;
|
|
193
|
+
_migratedStrandedLayers.add(key);
|
|
194
|
+
const legacyContentRoot = home.legacyRoot ? path_1.default.join(home.legacyRoot, 'personalized-employee') : null;
|
|
195
|
+
if (!legacyContentRoot || legacyContentRoot === home.contentRoot)
|
|
196
|
+
return;
|
|
197
|
+
if (!fs_1.default.existsSync(legacyContentRoot))
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
const walk = (absDir, relDir) => {
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = fs_1.default.readdirSync(absDir, { withFileTypes: true });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
210
|
+
const src = path_1.default.join(absDir, entry.name);
|
|
211
|
+
const dst = path_1.default.join(home.contentRoot, rel);
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
walk(src, rel);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!entry.isFile())
|
|
217
|
+
continue;
|
|
218
|
+
if (fs_1.default.existsSync(dst))
|
|
219
|
+
continue;
|
|
220
|
+
try {
|
|
221
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
|
|
222
|
+
fs_1.default.renameSync(src, dst);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
try {
|
|
226
|
+
fs_1.default.copyFileSync(src, dst);
|
|
227
|
+
fs_1.default.unlinkSync(src);
|
|
228
|
+
}
|
|
229
|
+
catch { /* leave source intact */ }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
walk(legacyContentRoot, '');
|
|
234
|
+
// Clean up empty dirs left behind (best-effort).
|
|
235
|
+
for (const dir of ORG_CONTENT_DIRS) {
|
|
236
|
+
try {
|
|
237
|
+
fs_1.default.rmdirSync(path_1.default.join(legacyContentRoot, dir));
|
|
238
|
+
}
|
|
239
|
+
catch { /* not empty — fine */ }
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
fs_1.default.rmdirSync(legacyContentRoot);
|
|
243
|
+
}
|
|
244
|
+
catch { /* not empty — fine */ }
|
|
245
|
+
}
|
|
246
|
+
catch { /* migration is best-effort; never fail a sync over it */ }
|
|
247
|
+
}
|
|
175
248
|
/**
|
|
176
249
|
* Content sitting at the standard local path that the configured home does not
|
|
177
250
|
* hold. Since the home is the single read location, this content is stranded:
|
|
@@ -72,11 +72,18 @@ function managerSecondaryLearningsBase() {
|
|
|
72
72
|
function getLearningRoots(workspaceRoot) {
|
|
73
73
|
const managerHome = (0, pack_home_1.resolvePackHome)('manager');
|
|
74
74
|
const managerHomeBase = (0, path_1.join)(managerHome.contentRoot, 'learnings');
|
|
75
|
+
const managerCacheBase = managerSecondaryLearningsBase();
|
|
76
|
+
// Derive the display base from the actual cache path relative to ~/.fraim so
|
|
77
|
+
// the display string always matches the filesystem path (fixed by #1043r2).
|
|
78
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
79
|
+
const managerCacheDisplayBase = managerCacheBase.startsWith(fraimDir)
|
|
80
|
+
? (0, project_fraim_paths_1.getUserFraimDisplayPath)(managerCacheBase.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
|
|
81
|
+
: managerCacheBase.replace(/\\/g, '/');
|
|
75
82
|
return {
|
|
76
83
|
globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
|
|
77
84
|
globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
|
|
78
|
-
managerCacheBase
|
|
79
|
-
managerCacheDisplayBase
|
|
85
|
+
managerCacheBase,
|
|
86
|
+
managerCacheDisplayBase,
|
|
80
87
|
managerHomeBase,
|
|
81
88
|
managerHomeDisplayBase: managerHomeBase.replace(/\\/g, '/'),
|
|
82
89
|
repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
|
|
@@ -637,11 +644,20 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
|
|
|
637
644
|
}
|
|
638
645
|
}
|
|
639
646
|
if (!orgCacheEligible) {
|
|
640
|
-
const
|
|
647
|
+
const managerHome = (0, pack_home_1.resolvePackHome)('manager');
|
|
648
|
+
const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
|
|
641
649
|
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
650
|
+
// Derive the display path relative to ~/.fraim. For the standard
|
|
651
|
+
// single-machine backend contentRoot is ~/.fraim/personalized-employee
|
|
652
|
+
// so the display path is ~/.fraim/personalized-employee/... For a
|
|
653
|
+
// synced backend it may be elsewhere (e.g. ~/.fraim/packs/manager-repo).
|
|
654
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
655
|
+
const relToFraim = managerCachePath.startsWith(fraimDir)
|
|
656
|
+
? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
|
|
657
|
+
: `personalized-employee/${relativePath}`;
|
|
642
658
|
return {
|
|
643
659
|
present: true,
|
|
644
|
-
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(
|
|
660
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim)
|
|
645
661
|
};
|
|
646
662
|
}
|
|
647
663
|
}
|
|
@@ -812,13 +828,18 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
812
828
|
}
|
|
813
829
|
}
|
|
814
830
|
if (key === 'manager' || key === 'managerRules') {
|
|
815
|
-
const
|
|
831
|
+
const managerHome = (0, pack_home_1.resolvePackHome)('manager');
|
|
832
|
+
const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
|
|
816
833
|
if ((0, fs_1.existsSync)(managerCachePath)) {
|
|
834
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
835
|
+
const relToFraim = managerCachePath.startsWith(fraimDir)
|
|
836
|
+
? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
|
|
837
|
+
: `personalized-employee/${relativePath}`;
|
|
817
838
|
return {
|
|
818
839
|
present: true,
|
|
819
840
|
readPath: managerCachePath,
|
|
820
841
|
writePath: '',
|
|
821
|
-
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(
|
|
842
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim),
|
|
822
843
|
scope,
|
|
823
844
|
managedByManagerSync: true
|
|
824
845
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.262",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"electron": "^41.2.2",
|
|
164
164
|
"electron-updater": "^6.8.9",
|
|
165
165
|
"express": "^5.2.1",
|
|
166
|
-
"fraim": "2.0.
|
|
166
|
+
"fraim": "2.0.262",
|
|
167
167
|
"mongodb": "^7.0.0",
|
|
168
168
|
"node-cron": "4.2.1",
|
|
169
169
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/review.css
CHANGED
|
@@ -638,3 +638,41 @@
|
|
|
638
638
|
display: flex;
|
|
639
639
|
gap: 8px;
|
|
640
640
|
}
|
|
641
|
+
/* Destination row: Company / This project toggle buttons. */
|
|
642
|
+
.lp-pp-dest-row {
|
|
643
|
+
display: flex;
|
|
644
|
+
gap: 8px;
|
|
645
|
+
}
|
|
646
|
+
.lp-pp-dest-btn {
|
|
647
|
+
flex: 1;
|
|
648
|
+
display: flex;
|
|
649
|
+
flex-direction: column;
|
|
650
|
+
align-items: flex-start;
|
|
651
|
+
gap: 2px;
|
|
652
|
+
padding: 8px 12px;
|
|
653
|
+
border-radius: 8px;
|
|
654
|
+
border: 1.5px solid var(--line);
|
|
655
|
+
background: var(--bg);
|
|
656
|
+
cursor: pointer;
|
|
657
|
+
text-align: left;
|
|
658
|
+
transition: border-color 0.12s, background 0.12s;
|
|
659
|
+
}
|
|
660
|
+
.lp-pp-dest-btn:hover { border-color: var(--accent, #0055cc); }
|
|
661
|
+
.lp-pp-dest-btn.lp-pp-dest-selected {
|
|
662
|
+
border-color: var(--accent, #0055cc);
|
|
663
|
+
background: color-mix(in srgb, var(--accent, #0055cc) 8%, var(--bg));
|
|
664
|
+
}
|
|
665
|
+
.lp-pp-dest-name {
|
|
666
|
+
font-size: 13px;
|
|
667
|
+
font-weight: 600;
|
|
668
|
+
color: var(--text);
|
|
669
|
+
}
|
|
670
|
+
.lp-pp-dest-sub {
|
|
671
|
+
font-size: 11px;
|
|
672
|
+
color: var(--muted);
|
|
673
|
+
}
|
|
674
|
+
.lp-pp-project-section {
|
|
675
|
+
display: flex;
|
|
676
|
+
flex-direction: column;
|
|
677
|
+
gap: 6px;
|
|
678
|
+
}
|
package/public/ai-hub/script.js
CHANGED
|
@@ -656,12 +656,12 @@ function taskPaneProjectEntries() {
|
|
|
656
656
|
return Array.from(byPath.values());
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
-
function taskPaneJobEntries() {
|
|
660
|
-
const jobs = [
|
|
661
|
-
{ id: '__freeform__', title: 'Ad-hoc instructions', intent: 'Start from the instructions below.' },
|
|
662
|
-
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
663
|
-
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
664
|
-
];
|
|
659
|
+
function taskPaneJobEntries() {
|
|
660
|
+
const jobs = [
|
|
661
|
+
{ id: '__freeform__', title: 'Ad-hoc instructions', intent: 'Start from the instructions below.' },
|
|
662
|
+
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
663
|
+
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
664
|
+
];
|
|
665
665
|
const seen = new Set();
|
|
666
666
|
return jobs
|
|
667
667
|
.filter((job) => job && job.id && !PAGE_SCOPED_JOBS.has(job.id))
|
|
@@ -715,7 +715,7 @@ function ensureTaskPaneLauncher() {
|
|
|
715
715
|
const fields = [
|
|
716
716
|
['Project', 'task-pane-project-select', 'select'],
|
|
717
717
|
['Agent tool', 'task-pane-employee-select', 'select'],
|
|
718
|
-
['Job', 'task-pane-job-select', 'select'],
|
|
718
|
+
['Job', 'task-pane-job-select', 'select'],
|
|
719
719
|
['Instructions', 'task-pane-instructions', 'textarea'],
|
|
720
720
|
];
|
|
721
721
|
fields.forEach(([labelText, id, tag]) => {
|
|
@@ -726,11 +726,11 @@ function ensureTaskPaneLauncher() {
|
|
|
726
726
|
span.textContent = labelText;
|
|
727
727
|
const input = document.createElement(tag);
|
|
728
728
|
input.id = id;
|
|
729
|
-
if (tag === 'textarea') {
|
|
730
|
-
input.rows = 4;
|
|
731
|
-
input.placeholder = 'Tell the employee what to do with this Office context.';
|
|
732
|
-
input.addEventListener('input', () => { input.dataset.userTouched = '1'; });
|
|
733
|
-
}
|
|
729
|
+
if (tag === 'textarea') {
|
|
730
|
+
input.rows = 4;
|
|
731
|
+
input.placeholder = 'Tell the employee what to do with this Office context.';
|
|
732
|
+
input.addEventListener('input', () => { input.dataset.userTouched = '1'; });
|
|
733
|
+
}
|
|
734
734
|
label.appendChild(span);
|
|
735
735
|
label.appendChild(input);
|
|
736
736
|
root.appendChild(label);
|
|
@@ -780,15 +780,15 @@ function renderTaskPaneLauncher() {
|
|
|
780
780
|
const employeeSelect = document.getElementById('task-pane-employee-select');
|
|
781
781
|
const jobSelect = document.getElementById('task-pane-job-select');
|
|
782
782
|
const instructions = document.getElementById('task-pane-instructions');
|
|
783
|
-
const start = document.getElementById('task-pane-start-job');
|
|
784
|
-
const current = document.getElementById('task-pane-current-job');
|
|
785
|
-
const context = document.getElementById('task-pane-context-summary');
|
|
786
|
-
const conv = typeof activeConversation === 'function' ? activeConversation() : null;
|
|
787
|
-
const priorJobId = jobSelect && jobSelect.value;
|
|
788
|
-
|
|
789
|
-
taskPaneSetOptions(projectSelect, projects, (project) => project.folderPath, (project) => project.name || friendlyProjectShortName(project.folderPath), state.projectPath);
|
|
790
|
-
taskPaneSetOptions(employeeSelect, agents, (agent) => agent.id, (agent) => agent.label || agent.id, state.selectedEmployeeId);
|
|
791
|
-
taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, priorJobId || '__freeform__');
|
|
783
|
+
const start = document.getElementById('task-pane-start-job');
|
|
784
|
+
const current = document.getElementById('task-pane-current-job');
|
|
785
|
+
const context = document.getElementById('task-pane-context-summary');
|
|
786
|
+
const conv = typeof activeConversation === 'function' ? activeConversation() : null;
|
|
787
|
+
const priorJobId = jobSelect && jobSelect.value;
|
|
788
|
+
|
|
789
|
+
taskPaneSetOptions(projectSelect, projects, (project) => project.folderPath, (project) => project.name || friendlyProjectShortName(project.folderPath), state.projectPath);
|
|
790
|
+
taskPaneSetOptions(employeeSelect, agents, (agent) => agent.id, (agent) => agent.label || agent.id, state.selectedEmployeeId);
|
|
791
|
+
taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, priorJobId || '__freeform__');
|
|
792
792
|
|
|
793
793
|
if (instructions && instructions.dataset.userTouched !== '1') {
|
|
794
794
|
instructions.value = taskPaneInstructionSeed();
|
|
@@ -834,19 +834,19 @@ async function taskPaneStartSelectedJob() {
|
|
|
834
834
|
const jobSelect = document.getElementById('task-pane-job-select');
|
|
835
835
|
const instructions = document.getElementById('task-pane-instructions');
|
|
836
836
|
const start = document.getElementById('task-pane-start-job');
|
|
837
|
-
const statusEl = document.getElementById('task-pane-start-status');
|
|
838
|
-
const selectedProject = projectSelect && projectSelect.value;
|
|
839
|
-
const requestedEmployeeId = employeeSelect && employeeSelect.value;
|
|
840
|
-
const requestedJobId = jobSelect && jobSelect.value;
|
|
841
|
-
if (selectedProject && tfCanonicalProjectPath(selectedProject) !== tfCanonicalProjectPath(state.projectPath)) {
|
|
842
|
-
await taskPaneSwitchProject(selectedProject);
|
|
843
|
-
const updatedEmployeeSelect = document.getElementById('task-pane-employee-select');
|
|
844
|
-
if (updatedEmployeeSelect && requestedEmployeeId) updatedEmployeeSelect.value = requestedEmployeeId;
|
|
845
|
-
const updatedJobSelect = document.getElementById('task-pane-job-select');
|
|
846
|
-
if (updatedJobSelect && requestedJobId) updatedJobSelect.value = requestedJobId;
|
|
847
|
-
}
|
|
848
|
-
const jobs = taskPaneJobEntries();
|
|
849
|
-
const job = jobs.find((entry) => entry.id === requestedJobId);
|
|
837
|
+
const statusEl = document.getElementById('task-pane-start-status');
|
|
838
|
+
const selectedProject = projectSelect && projectSelect.value;
|
|
839
|
+
const requestedEmployeeId = employeeSelect && employeeSelect.value;
|
|
840
|
+
const requestedJobId = jobSelect && jobSelect.value;
|
|
841
|
+
if (selectedProject && tfCanonicalProjectPath(selectedProject) !== tfCanonicalProjectPath(state.projectPath)) {
|
|
842
|
+
await taskPaneSwitchProject(selectedProject);
|
|
843
|
+
const updatedEmployeeSelect = document.getElementById('task-pane-employee-select');
|
|
844
|
+
if (updatedEmployeeSelect && requestedEmployeeId) updatedEmployeeSelect.value = requestedEmployeeId;
|
|
845
|
+
const updatedJobSelect = document.getElementById('task-pane-job-select');
|
|
846
|
+
if (updatedJobSelect && requestedJobId) updatedJobSelect.value = requestedJobId;
|
|
847
|
+
}
|
|
848
|
+
const jobs = taskPaneJobEntries();
|
|
849
|
+
const job = jobs.find((entry) => entry.id === requestedJobId);
|
|
850
850
|
const employeeId = requestedEmployeeId || (employeeSelect && employeeSelect.value);
|
|
851
851
|
if (!job || !employeeId) return;
|
|
852
852
|
if (start) start.disabled = true;
|
|
@@ -10894,7 +10894,13 @@ function tfRenderProjectContextTop() {
|
|
|
10894
10894
|
{ ico: '🧠', label: 'Sleep on learnings', onClick: () => tfRunShareLearnings('project') },
|
|
10895
10895
|
{ ico: '↑', label: 'Share with others', onClick: () => {
|
|
10896
10896
|
const h = document.getElementById('project-learnings');
|
|
10897
|
-
if (h)
|
|
10897
|
+
if (!h) return;
|
|
10898
|
+
const entering = h.dataset.lpShareMode !== 'true';
|
|
10899
|
+
h.dataset.lpShareMode = entering ? 'true' : 'false';
|
|
10900
|
+
const acc = document.getElementById('proj-learnings-acc');
|
|
10901
|
+
const actionRow = acc && acc.querySelector('.ctx-acc-action-row');
|
|
10902
|
+
if (actionRow) actionRow.hidden = entering;
|
|
10903
|
+
tfReRenderScope('manager', 'project');
|
|
10898
10904
|
}},
|
|
10899
10905
|
]);
|
|
10900
10906
|
}
|
|
@@ -12462,7 +12468,15 @@ function tfRenderManager() {
|
|
|
12462
12468
|
// promotion); only the share action.
|
|
12463
12469
|
tfEnsureAccAction('manager-learn-acc', '↑', 'Share with others', () => {
|
|
12464
12470
|
const h = document.getElementById('manager-learnings');
|
|
12465
|
-
if (h)
|
|
12471
|
+
if (!h) return;
|
|
12472
|
+
const entering = h.dataset.lpShareMode !== 'true';
|
|
12473
|
+
h.dataset.lpShareMode = entering ? 'true' : 'false';
|
|
12474
|
+
// Hide the accordion action row while in share mode so the top button
|
|
12475
|
+
// doesn't duplicate the bottom share bar.
|
|
12476
|
+
const acc = document.getElementById('manager-learn-acc');
|
|
12477
|
+
const actionRow = acc && acc.querySelector('.ctx-acc-action-row');
|
|
12478
|
+
if (actionRow) actionRow.hidden = entering;
|
|
12479
|
+
tfReRenderScope('manager', 'machine');
|
|
12466
12480
|
});
|
|
12467
12481
|
// Issue #540 R4-R7: render manager team pool on every manager-tab activation.
|
|
12468
12482
|
renderManagerTeamPool();
|
|
@@ -13119,6 +13133,15 @@ function tfUpdateShareBar(host, scope, level) {
|
|
|
13119
13133
|
cancelBtn.addEventListener('click', () => {
|
|
13120
13134
|
sel.clear();
|
|
13121
13135
|
host.dataset.lpShareMode = 'false';
|
|
13136
|
+
// Restore the accordion action row that was hidden when share mode started.
|
|
13137
|
+
const accId = scope === 'manager' && level === 'machine' ? 'manager-learn-acc'
|
|
13138
|
+
: scope === 'manager' && level === 'project' ? 'proj-learnings-acc'
|
|
13139
|
+
: null;
|
|
13140
|
+
if (accId) {
|
|
13141
|
+
const acc = document.getElementById(accId);
|
|
13142
|
+
const actionRow = acc && acc.querySelector('.ctx-acc-action-row');
|
|
13143
|
+
if (actionRow) actionRow.hidden = false;
|
|
13144
|
+
}
|
|
13122
13145
|
tfReRenderScope(scope, level);
|
|
13123
13146
|
});
|
|
13124
13147
|
bar.appendChild(cancelBtn);
|
|
@@ -13138,64 +13161,120 @@ async function tfRunShareWithOthers(scope, level, selectedTitles) {
|
|
|
13138
13161
|
if (typeof showStatus === 'function') showStatus('Pick a project folder first, then share learnings.', true);
|
|
13139
13162
|
return;
|
|
13140
13163
|
}
|
|
13141
|
-
//
|
|
13142
|
-
//
|
|
13143
|
-
//
|
|
13144
|
-
|
|
13145
|
-
|
|
13146
|
-
if (projects.length > 1 && tf.area === 'manager') {
|
|
13147
|
-
tfShareWithOthersPicker(scope, level, selectedTitles, job);
|
|
13148
|
-
return;
|
|
13149
|
-
}
|
|
13150
|
-
await tfLaunchShareWithOthers(scope, level, selectedTitles, job, tf.activeProjectId);
|
|
13164
|
+
// Always show the destination picker when sharing from the Manager tab —
|
|
13165
|
+
// the manager must choose Company vs. This project regardless of how many
|
|
13166
|
+
// projects are loaded.
|
|
13167
|
+
const hostId = scope === 'manager' && level === 'machine' ? 'manager-learnings' : 'project-learnings';
|
|
13168
|
+
tfShareDestinationPicker(hostId, scope, level, selectedTitles, job);
|
|
13151
13169
|
}
|
|
13152
13170
|
|
|
13153
|
-
//
|
|
13154
|
-
//
|
|
13155
|
-
function
|
|
13156
|
-
const host = document.getElementById(
|
|
13171
|
+
// Destination picker: first ask Company vs. This project, then (for project)
|
|
13172
|
+
// optionally ask which project when more than one is loaded.
|
|
13173
|
+
function tfShareDestinationPicker(hostId, scope, level, selectedTitles, job) {
|
|
13174
|
+
const host = document.getElementById(hostId);
|
|
13157
13175
|
if (!host) return;
|
|
13158
|
-
//
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
picker = document.createElement('div');
|
|
13176
|
+
// Remove any existing picker before showing a fresh one.
|
|
13177
|
+
host.querySelectorAll('.lp-project-picker').forEach((el) => el.remove());
|
|
13178
|
+
|
|
13179
|
+
const picker = document.createElement('div');
|
|
13162
13180
|
picker.className = 'lp-project-picker';
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
picker.appendChild(
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13181
|
+
|
|
13182
|
+
// ── Step 1: destination scope ──────────────────────────────────────────
|
|
13183
|
+
const destLabel = document.createElement('div');
|
|
13184
|
+
destLabel.className = 'lp-pp-label';
|
|
13185
|
+
destLabel.textContent = 'Share to:';
|
|
13186
|
+
picker.appendChild(destLabel);
|
|
13187
|
+
|
|
13188
|
+
const destRow = document.createElement('div');
|
|
13189
|
+
destRow.className = 'lp-pp-dest-row';
|
|
13190
|
+
|
|
13191
|
+
const makeDestBtn = (value, text, desc) => {
|
|
13192
|
+
const btn = document.createElement('button');
|
|
13193
|
+
btn.type = 'button';
|
|
13194
|
+
btn.className = 'lp-pp-dest-btn';
|
|
13195
|
+
btn.dataset.dest = value;
|
|
13196
|
+
const name = document.createElement('span'); name.className = 'lp-pp-dest-name'; name.textContent = text;
|
|
13197
|
+
const sub = document.createElement('span'); sub.className = 'lp-pp-dest-sub'; sub.textContent = desc;
|
|
13198
|
+
btn.appendChild(name); btn.appendChild(sub);
|
|
13199
|
+
return btn;
|
|
13200
|
+
};
|
|
13201
|
+
|
|
13202
|
+
const companyBtn = makeDestBtn('org', 'Company', 'Everyone in the organization');
|
|
13203
|
+
const projectBtn = makeDestBtn('project', 'This project', 'Visible on a specific project');
|
|
13204
|
+
destRow.appendChild(companyBtn);
|
|
13205
|
+
destRow.appendChild(projectBtn);
|
|
13206
|
+
picker.appendChild(destRow);
|
|
13207
|
+
|
|
13208
|
+
// ── Step 2: project picker (hidden until "This project" chosen) ────────
|
|
13209
|
+
const projectSection = document.createElement('div');
|
|
13210
|
+
projectSection.className = 'lp-pp-project-section';
|
|
13211
|
+
projectSection.hidden = true;
|
|
13212
|
+
|
|
13213
|
+
const projects = (typeof tf !== 'undefined' && tf.projects) || [];
|
|
13214
|
+
if (projects.length > 1) {
|
|
13215
|
+
const projLabel = document.createElement('label');
|
|
13216
|
+
projLabel.className = 'lp-pp-label';
|
|
13217
|
+
const projLabelId = 'lp-pp-proj-' + Math.random().toString(36).slice(2, 8);
|
|
13218
|
+
projLabel.id = projLabelId;
|
|
13219
|
+
projLabel.textContent = 'Which project?';
|
|
13220
|
+
const projSel = document.createElement('select');
|
|
13221
|
+
projSel.className = 'lp-pp-sel';
|
|
13222
|
+
projSel.setAttribute('aria-labelledby', projLabelId);
|
|
13223
|
+
for (const p of projects) {
|
|
13224
|
+
const o = document.createElement('option'); o.value = p.id;
|
|
13225
|
+
o.textContent = p.name || p.id;
|
|
13226
|
+
if (p.id === tf.activeProjectId) o.selected = true;
|
|
13227
|
+
projSel.appendChild(o);
|
|
13228
|
+
}
|
|
13229
|
+
projectSection.appendChild(projLabel);
|
|
13230
|
+
projectSection.appendChild(projSel);
|
|
13231
|
+
projectSection._projSel = projSel;
|
|
13232
|
+
}
|
|
13233
|
+
picker.appendChild(projectSection);
|
|
13234
|
+
|
|
13235
|
+
// ── Action row ─────────────────────────────────────────────────────────
|
|
13179
13236
|
const bar = document.createElement('div'); bar.className = 'lp-pp-bar';
|
|
13180
|
-
const go = document.createElement('button');
|
|
13181
|
-
|
|
13237
|
+
const go = document.createElement('button');
|
|
13238
|
+
go.type = 'button'; go.className = 'rb-approve'; go.textContent = 'Start'; go.disabled = true;
|
|
13239
|
+
const cancel = document.createElement('button');
|
|
13240
|
+
cancel.type = 'button'; cancel.className = 'rb-secondary'; cancel.textContent = 'Cancel';
|
|
13182
13241
|
bar.appendChild(go); bar.appendChild(cancel);
|
|
13183
13242
|
picker.appendChild(bar);
|
|
13184
13243
|
host.appendChild(picker);
|
|
13244
|
+
|
|
13245
|
+
let chosenDest = null;
|
|
13246
|
+
|
|
13247
|
+
const selectDest = (btn) => {
|
|
13248
|
+
destRow.querySelectorAll('.lp-pp-dest-btn').forEach((b) => b.classList.remove('lp-pp-dest-selected'));
|
|
13249
|
+
btn.classList.add('lp-pp-dest-selected');
|
|
13250
|
+
chosenDest = btn.dataset.dest;
|
|
13251
|
+
projectSection.hidden = chosenDest !== 'project';
|
|
13252
|
+
go.disabled = false;
|
|
13253
|
+
};
|
|
13254
|
+
|
|
13255
|
+
companyBtn.addEventListener('click', () => selectDest(companyBtn));
|
|
13256
|
+
projectBtn.addEventListener('click', () => selectDest(projectBtn));
|
|
13257
|
+
|
|
13185
13258
|
cancel.addEventListener('click', () => picker.remove());
|
|
13259
|
+
|
|
13186
13260
|
go.addEventListener('click', async () => {
|
|
13187
13261
|
go.disabled = true;
|
|
13188
13262
|
picker.remove();
|
|
13189
|
-
|
|
13263
|
+
const projectId = chosenDest === 'project'
|
|
13264
|
+
? (projectSection._projSel ? projectSection._projSel.value : (tf.activeProjectId || state.projectPath))
|
|
13265
|
+
: (tf.activeProjectId || state.projectPath);
|
|
13266
|
+
await tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, chosenDest);
|
|
13190
13267
|
});
|
|
13191
13268
|
}
|
|
13192
13269
|
|
|
13193
|
-
async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId) {
|
|
13270
|
+
async function tfLaunchShareWithOthers(scope, level, selectedTitles, job, projectId, destination) {
|
|
13271
|
+
const dest = destination || 'project';
|
|
13194
13272
|
let msg;
|
|
13195
13273
|
if (selectedTitles && selectedTitles.length > 0) {
|
|
13196
13274
|
const levelWord = level === 'project' ? 'project' : level === 'org' ? 'company' : 'manager';
|
|
13275
|
+
const destWord = dest === 'org' ? 'company (org) level' : 'project level';
|
|
13197
13276
|
const list = selectedTitles.map((t) => `- "${t}"`).join('\n');
|
|
13198
|
-
msg = `Run share-with-others. I want to share these ${levelWord}-level learnings:\n${list}\nShow me the
|
|
13277
|
+
msg = `Run share-with-others. I want to share these ${levelWord}-level learnings to the ${destWord}:\n${list}\nShow me the de-personalisation plan for each, then apply it.`;
|
|
13199
13278
|
} else {
|
|
13200
13279
|
msg = 'Run share-with-others. Show me everything I own at each level, grouped by type, so I can decide what to move.';
|
|
13201
13280
|
}
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -4205,14 +4205,18 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4205
4205
|
.modal-hdr { padding: 18px 22px; border-bottom: 1px solid var(--line); }
|
|
4206
4206
|
.modal-hdr h2 { font-size: 18px; font-weight: 700; margin: 0 0 4px; }
|
|
4207
4207
|
.modal-hdr p { font-size: 13px; color: var(--muted); margin: 0; line-height: 1.5; }
|
|
4208
|
-
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
4209
|
-
.modal-close { float: right; background: var(--bg); border: none; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; cursor: pointer; color: var(--muted); line-height: 1; }
|
|
4210
|
-
.modal-close:hover { color: var(--text); }
|
|
4211
|
-
.
|
|
4212
|
-
.aom-cancel-btn
|
|
4213
|
-
.
|
|
4214
|
-
.
|
|
4215
|
-
.modal-
|
|
4208
|
+
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
4209
|
+
.modal-close { float: right; background: var(--bg); border: none; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; cursor: pointer; color: var(--muted); line-height: 1; }
|
|
4210
|
+
.modal-close:hover { color: var(--text); }
|
|
4211
|
+
.modal-close:focus, .modal-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4212
|
+
.aom-cancel-btn { background: var(--surface); border: 1px solid color-mix(in srgb, var(--accent-strong) 52%, var(--line)); border-radius: 20px; padding: 9px 20px; font-size: 14px; cursor: pointer; color: var(--accent-strong); }
|
|
4213
|
+
.aom-cancel-btn:hover { border-color: var(--accent-strong); color: var(--accent-strong); }
|
|
4214
|
+
.aom-cancel-btn:focus, .aom-cancel-btn:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4215
|
+
.modal-footer { padding: 12px 22px 20px; display: flex; align-items: center; justify-content: space-between; }
|
|
4216
|
+
.modal-next { padding: 9px 22px; background: var(--accent-strong); color: var(--bg); border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
4217
|
+
.modal-next:hover { opacity: .9; }
|
|
4218
|
+
.modal-next:focus, .modal-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4219
|
+
.modal-back { font-size: 13px; color: var(--muted); background: none; border: none; cursor: pointer; }
|
|
4216
4220
|
.modal-step-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 18px; }
|
|
4217
4221
|
.modal-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
|
|
4218
4222
|
.modal-dot.on { background: var(--text); }
|
|
@@ -4669,7 +4673,8 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4669
4673
|
align-items: center;
|
|
4670
4674
|
justify-content: center;
|
|
4671
4675
|
}
|
|
4672
|
-
.np-close:hover { color: var(--text); }
|
|
4676
|
+
.np-close:hover { color: var(--text); }
|
|
4677
|
+
.np-close:focus, .np-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4673
4678
|
|
|
4674
4679
|
.np-step-dots {
|
|
4675
4680
|
display: flex;
|
|
@@ -4731,26 +4736,29 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4731
4736
|
justify-content: space-between;
|
|
4732
4737
|
}
|
|
4733
4738
|
|
|
4734
|
-
.np-next {
|
|
4735
|
-
padding: 9px 22px;
|
|
4736
|
-
background: var(--
|
|
4737
|
-
color:
|
|
4738
|
-
border: none;
|
|
4739
|
-
border-radius: 20px;
|
|
4740
|
-
font-size: 14px;
|
|
4741
|
-
font-weight: 600;
|
|
4742
|
-
cursor: pointer;
|
|
4743
|
-
}
|
|
4744
|
-
.np-next:hover { opacity: .88; }
|
|
4745
|
-
|
|
4746
|
-
.np-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4739
|
+
.np-next {
|
|
4740
|
+
padding: 9px 22px;
|
|
4741
|
+
background: var(--accent-strong);
|
|
4742
|
+
color: var(--bg);
|
|
4743
|
+
border: none;
|
|
4744
|
+
border-radius: 20px;
|
|
4745
|
+
font-size: 14px;
|
|
4746
|
+
font-weight: 600;
|
|
4747
|
+
cursor: pointer;
|
|
4748
|
+
}
|
|
4749
|
+
.np-next:hover { opacity: .88; }
|
|
4750
|
+
.np-next:focus, .np-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4751
|
+
.np-next:disabled { opacity: .55; cursor: not-allowed; }
|
|
4752
|
+
|
|
4753
|
+
.np-back {
|
|
4754
|
+
font-size: 13px;
|
|
4755
|
+
color: var(--accent-strong);
|
|
4756
|
+
background: none;
|
|
4757
|
+
border: none;
|
|
4758
|
+
cursor: pointer;
|
|
4759
|
+
}
|
|
4760
|
+
.np-back:hover { color: var(--accent-strong); text-decoration: underline; }
|
|
4761
|
+
.np-back:focus, .np-back:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4754
4762
|
|
|
4755
4763
|
/* ── Discard-guard confirm (shown when a dirty wizard is dismissed) ── */
|
|
4756
4764
|
.np-discard {
|
|
@@ -4775,10 +4783,11 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4775
4783
|
.np-discard-msg { font-size: 13px; color: var(--muted); margin: 0 0 18px; line-height: 1.5; }
|
|
4776
4784
|
.np-discard-row { display: flex; gap: 8px; justify-content: flex-end; }
|
|
4777
4785
|
.np-discard-row button { font: inherit; font-size: 13px; font-weight: 600; border-radius: 9px; padding: 8px 16px; cursor: pointer; }
|
|
4778
|
-
.np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
|
|
4779
|
-
.np-discard-keep:hover { border-color: var(--muted); }
|
|
4780
|
-
.np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
|
|
4781
|
-
.np-discard-yes:hover { opacity: .9; }
|
|
4786
|
+
.np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
|
|
4787
|
+
.np-discard-keep:hover { border-color: var(--muted); }
|
|
4788
|
+
.np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
|
|
4789
|
+
.np-discard-yes:hover { opacity: .9; }
|
|
4790
|
+
.np-discard-row button:focus, .np-discard-row button:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4782
4791
|
|
|
4783
4792
|
/* ── D11: Source attachment row (step 2) ── */
|
|
4784
4793
|
.np-source {
|
|
@@ -5265,8 +5274,9 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
5265
5274
|
/* ── New-project: folder picker row ── */
|
|
5266
5275
|
.np-folder-row { display:flex; gap:8px; }
|
|
5267
5276
|
.np-folder-row input { flex:1; }
|
|
5268
|
-
.np-browse-btn { padding:9px 16px; background:var(--
|
|
5269
|
-
.np-browse-btn:hover { opacity:.85; }
|
|
5277
|
+
.np-browse-btn { padding:9px 16px; background:var(--accent-strong); color:var(--bg); border:none; border-radius:9px; font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
|
5278
|
+
.np-browse-btn:hover { opacity:.85; }
|
|
5279
|
+
.np-browse-btn:focus, .np-browse-btn:focus-visible { outline:2px solid var(--accent-strong); outline-offset:2px; }
|
|
5270
5280
|
.np-field-hint { font-size:11px; color:var(--muted); margin-top:5px; line-height:1.5; }
|
|
5271
5281
|
|
|
5272
5282
|
@media (max-width: 520px) {
|