fraim-framework 2.0.187 → 2.0.189
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/desktop-main.js +15 -1
- package/dist/src/ai-hub/server.js +46 -20
- package/dist/src/core/ai-mentor.js +11 -2
- package/package.json +1 -1
- package/public/ai-hub/index.html +19 -5
- package/public/ai-hub/script.js +162 -35
- package/public/ai-hub/styles.css +70 -7
|
@@ -10,6 +10,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const server_1 = require("./server");
|
|
11
11
|
const cert_store_1 = require("./cert-store");
|
|
12
12
|
const office_sideload_1 = require("./office-sideload");
|
|
13
|
+
const remote_hub_gateway_1 = require("./remote-hub-gateway");
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// State
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -50,6 +51,17 @@ function applyUserDataOverride() {
|
|
|
50
51
|
fs_1.default.mkdirSync(userDataDir, { recursive: true });
|
|
51
52
|
electron_1.app.setPath('userData', userDataDir);
|
|
52
53
|
}
|
|
54
|
+
function isTrustedInAppNavigation(targetUrl, hubUrl) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(targetUrl);
|
|
57
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
58
|
+
return false;
|
|
59
|
+
return parsed.origin === new URL(hubUrl).origin || parsed.origin === new URL((0, remote_hub_gateway_1.resolveFraimRemoteUrl)()).origin;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
53
65
|
// ---------------------------------------------------------------------------
|
|
54
66
|
// Tray icon resolution — prefers bundled icon, falls back to a 1×1 empty image
|
|
55
67
|
// so the app never crashes if assets aren't present.
|
|
@@ -204,7 +216,9 @@ async function createWindow(url) {
|
|
|
204
216
|
mainWindow.on('closed', () => electron_1.nativeTheme.removeListener('updated', applyOverlay));
|
|
205
217
|
}
|
|
206
218
|
mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
|
|
207
|
-
|
|
219
|
+
if (isTrustedInAppNavigation(targetUrl, url)) {
|
|
220
|
+
void mainWindow?.loadURL(targetUrl);
|
|
221
|
+
}
|
|
208
222
|
return { action: 'deny' };
|
|
209
223
|
});
|
|
210
224
|
mainWindow.once('ready-to-show', () => mainWindow?.show());
|
|
@@ -128,6 +128,17 @@ function buildHubPersonaHireUrl(personaKey, hireMode = 'job') {
|
|
|
128
128
|
const params = new URLSearchParams({ persona: personaKey, mode: hireMode });
|
|
129
129
|
return `/pricing?${params.toString()}`;
|
|
130
130
|
}
|
|
131
|
+
function buildHostedAuthUrl(surface, redirectTo) {
|
|
132
|
+
const target = new URL('/auth/sign-in.html', (0, remote_hub_gateway_1.resolveFraimRemoteUrl)());
|
|
133
|
+
target.searchParams.set('surface', surface);
|
|
134
|
+
target.searchParams.set('redirect_to', redirectTo);
|
|
135
|
+
return target.toString();
|
|
136
|
+
}
|
|
137
|
+
function buildHostedPathUrl(pathname, queryString) {
|
|
138
|
+
const target = new URL(pathname, (0, remote_hub_gateway_1.resolveFraimRemoteUrl)());
|
|
139
|
+
target.search = queryString;
|
|
140
|
+
return target.toString();
|
|
141
|
+
}
|
|
131
142
|
function buildHubPersonaAvatarUrl(personaKey) {
|
|
132
143
|
return loadPersonaHiringModule()?.buildPersonaAvatarUrl(personaKey) ?? '';
|
|
133
144
|
}
|
|
@@ -1407,6 +1418,7 @@ class AiHubServer {
|
|
|
1407
1418
|
const activeRun = latest ? this.enrichRunForResponse(latest) : undefined;
|
|
1408
1419
|
return {
|
|
1409
1420
|
title: 'AI Hub',
|
|
1421
|
+
remoteBaseUrl: (0, remote_hub_gateway_1.resolveFraimRemoteUrl)(),
|
|
1410
1422
|
project,
|
|
1411
1423
|
preferences,
|
|
1412
1424
|
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath, catalogOptions),
|
|
@@ -1997,26 +2009,40 @@ class AiHubServer {
|
|
|
1997
2009
|
// Issue #512: Serve the account and analytics pages from public/.
|
|
1998
2010
|
// These live outside of /ai-hub so they need explicit routes.
|
|
1999
2011
|
const publicDir = resolveAiHubPublicDir().replace(/[\\/]ai-hub$/, '');
|
|
2000
|
-
this.
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
res.
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
res.
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2012
|
+
if (!this.dbService) {
|
|
2013
|
+
this.app.get(/^\/auth(\/.*)?$/, (req, res) => {
|
|
2014
|
+
const parsed = new URL(req.originalUrl, 'http://127.0.0.1');
|
|
2015
|
+
res.redirect(buildHostedPathUrl(parsed.pathname, parsed.search));
|
|
2016
|
+
});
|
|
2017
|
+
this.app.get(['/account', '/account/'], (_req, res) => {
|
|
2018
|
+
res.redirect(buildHostedAuthUrl('account', '/account/'));
|
|
2019
|
+
});
|
|
2020
|
+
this.app.get(['/analytics', '/analytics/'], (_req, res) => {
|
|
2021
|
+
res.redirect(buildHostedAuthUrl('analytics', '/analytics/'));
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
this.app.get('/account', (_req, res) => {
|
|
2026
|
+
const filePath = path_1.default.join(publicDir, 'account', 'index.html');
|
|
2027
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
2028
|
+
res.sendFile(filePath);
|
|
2029
|
+
}
|
|
2030
|
+
else {
|
|
2031
|
+
res.status(404).send('Account page not found.');
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
this.app.use('/account', express_1.default.static(path_1.default.join(publicDir, 'account')));
|
|
2035
|
+
this.app.get('/analytics', (_req, res) => {
|
|
2036
|
+
const filePath = path_1.default.join(publicDir, 'analytics', 'index.html');
|
|
2037
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
2038
|
+
res.sendFile(filePath);
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
res.status(404).send('Analytics page not found.');
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
this.app.use('/analytics', express_1.default.static(path_1.default.join(publicDir, 'analytics')));
|
|
2045
|
+
}
|
|
2020
2046
|
// Issue #478: Serve the PowerPoint task pane HTML and manifest.
|
|
2021
2047
|
// Office JS appends query strings (?_host_Info=PowerPoint$Win32$...) to every
|
|
2022
2048
|
// request, so we must strip them before resolving the file path. Use a custom
|
|
@@ -96,7 +96,7 @@ class AIMentor {
|
|
|
96
96
|
const metadataPhases = Object.keys(workflow.metadata.phases || {});
|
|
97
97
|
const initialPhase = workflow.metadata.initialPhase || metadataPhases[0] || Array.from(workflow.phases.keys())[0] || 'starting';
|
|
98
98
|
const overview = workflow.overview.trim();
|
|
99
|
-
|
|
99
|
+
const lines = [
|
|
100
100
|
overview,
|
|
101
101
|
'',
|
|
102
102
|
'---',
|
|
@@ -104,7 +104,16 @@ class AIMentor {
|
|
|
104
104
|
'## Start Here',
|
|
105
105
|
`- Initial phase: \`${initialPhase}\``,
|
|
106
106
|
'- Call `seekMentoring` to load the instructions for this phase.'
|
|
107
|
-
]
|
|
107
|
+
];
|
|
108
|
+
// A job that produces reviewable repository changes must resolve its workspace
|
|
109
|
+
// before starting. The mode conditional (branch/worktree vs in place) lives in
|
|
110
|
+
// the skill and is evaluated by the agent from its own local settings.
|
|
111
|
+
if (workflow.metadata.producesRepoChanges === true) {
|
|
112
|
+
lines.push('', '## Set Up Your Workspace First', '- This job changes the repository. Before phase work, read and follow'
|
|
113
|
+
+ ' `get_fraim_file({ path: "skills/engineering/set-up-workspace.md" })`'
|
|
114
|
+
+ ' to work on the issue\'s branch when a repository is configured, or in place otherwise.');
|
|
115
|
+
}
|
|
116
|
+
return lines.join('\n');
|
|
108
117
|
}
|
|
109
118
|
async generateStartingMessage(workflow, phaseId, skipIncludes) {
|
|
110
119
|
const entityType = 'Job';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.189",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/public/ai-hub/index.html
CHANGED
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
<div class="am-email" id="am-email"></div>
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
|
-
<
|
|
36
|
+
<button class="am-item" id="am-account" type="button">
|
|
37
37
|
<span class="am-ico">👤</span>
|
|
38
38
|
<div><div class="am-label">Account</div><div class="am-sub">API key, sessions, billing</div></div>
|
|
39
|
-
</
|
|
39
|
+
</button>
|
|
40
40
|
<button class="am-item" id="am-brain" type="button">
|
|
41
41
|
<span class="am-ico">🧠</span>
|
|
42
42
|
<div><div class="am-label">Brain</div><div class="am-sub">Everything your team knows</div></div>
|
|
43
43
|
</button>
|
|
44
|
-
<
|
|
44
|
+
<button class="am-item" id="am-analytics" type="button">
|
|
45
45
|
<span class="am-ico">📊</span>
|
|
46
46
|
<div><div class="am-label">Analytics</div><div class="am-sub">Usage, cost, and ROI</div></div>
|
|
47
|
-
</
|
|
47
|
+
</button>
|
|
48
48
|
<button class="am-item am-theme" id="am-theme-toggle" type="button" role="switch" aria-checked="false">
|
|
49
49
|
<span class="am-ico" id="am-theme-ico">🌙</span>
|
|
50
50
|
<div><div class="am-label">Dark mode</div><div class="am-sub" id="am-theme-sub">Off</div></div>
|
|
@@ -141,7 +141,21 @@
|
|
|
141
141
|
</div>
|
|
142
142
|
</section>
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
<section class="hub-area" id="area-connected" hidden>
|
|
145
|
+
<div class="connected-shell">
|
|
146
|
+
<div class="connected-toolbar">
|
|
147
|
+
<div class="connected-title-block">
|
|
148
|
+
<div class="connected-kicker" id="connected-kicker">Connected FRAIM</div>
|
|
149
|
+
<div class="connected-title" id="connected-title">Account</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="connected-origin" id="connected-origin"></div>
|
|
152
|
+
<button class="connected-close" id="connected-close" type="button">Back</button>
|
|
153
|
+
</div>
|
|
154
|
+
<iframe id="connected-frame" class="connected-frame" title="FRAIM connected surface" allow="clipboard-read; clipboard-write"></iframe>
|
|
155
|
+
</div>
|
|
156
|
+
</section>
|
|
157
|
+
|
|
158
|
+
<!-- Issue #512: Projects area wraps the existing single-surface Hub. -->
|
|
145
159
|
<section class="hub-area on" id="area-projects">
|
|
146
160
|
<div class="proj-tabs" id="proj-tabs">
|
|
147
161
|
<button class="ptab on" id="ptab-overview" type="button" data-view="overview">Overview</button>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -45,9 +45,11 @@ const state = {
|
|
|
45
45
|
selectedJob: null, // chosen in modal step 1
|
|
46
46
|
selectedEmployeeId: null,
|
|
47
47
|
selectedPersonaKey: null, // R4: null = "All employees"
|
|
48
|
-
modalPersonaFilter: null, // R3+: filter inside new-job modal; null = "All jobs"
|
|
49
|
-
storedApiKey: null, // R2: loaded from preferences, sent on bootstrap
|
|
50
|
-
|
|
48
|
+
modalPersonaFilter: null, // R3+: filter inside new-job modal; null = "All jobs"
|
|
49
|
+
storedApiKey: null, // R2: loaded from preferences, sent on bootstrap
|
|
50
|
+
remoteBaseUrl: null,
|
|
51
|
+
connectedReturnArea: 'projects',
|
|
52
|
+
panelState: {}, // { [convId]: { coach?: boolean } }
|
|
51
53
|
wordContext: null, // WordContext pushed from taskpane.html via postMessage
|
|
52
54
|
// Pending coaching job selected via a quick-coach button or template picker.
|
|
53
55
|
// Sent as coachingJobId in the next continueRun call and then cleared.
|
|
@@ -153,10 +155,14 @@ function cacheBootstrapPayload(bootstrap, docUrl) {
|
|
|
153
155
|
bootstrapCache.set(bootstrapCacheKey(bootstrap.project.path, docUrl), bootstrap);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
function applyBootstrap(bootstrap, docUrl) {
|
|
157
|
-
state.bootstrap = bootstrap;
|
|
158
|
-
state.projectPath = bootstrap.project.path;
|
|
159
|
-
state.
|
|
158
|
+
function applyBootstrap(bootstrap, docUrl) {
|
|
159
|
+
state.bootstrap = bootstrap;
|
|
160
|
+
state.projectPath = bootstrap.project.path;
|
|
161
|
+
state.remoteBaseUrl = bootstrap.remoteBaseUrl || null;
|
|
162
|
+
if (bootstrap.preferences && bootstrap.preferences.apiKey) {
|
|
163
|
+
state.storedApiKey = bootstrap.preferences.apiKey;
|
|
164
|
+
}
|
|
165
|
+
state.selectedEmployeeId = state.selectedEmployeeId || bootstrap.preferences.employeeId;
|
|
160
166
|
// Restore persona selection persisted on the server side.
|
|
161
167
|
if (bootstrap.preferences && bootstrap.preferences.personaKey !== undefined) {
|
|
162
168
|
state.selectedPersonaKey = bootstrap.preferences.personaKey;
|
|
@@ -1014,6 +1020,7 @@ function renderRail() {
|
|
|
1014
1020
|
// assigned — even before they have a run, so you can assign/delegate to them.
|
|
1015
1021
|
const tfProjId = (typeof tf !== 'undefined' && tf) ? tf.activeProjectId : null;
|
|
1016
1022
|
const managerTeamKeys = (state.bootstrap?.managerTeam || []).map((e) => e.personaKey).filter(Boolean);
|
|
1023
|
+
const managerTeamKeySet = new Set(managerTeamKeys);
|
|
1017
1024
|
// merge: master resolves the tree's employees via tfProjectTeamKeys() (hired personas),
|
|
1018
1025
|
// not project.team; align on it so every hired employee appears in the single section.
|
|
1019
1026
|
const projectEmployeeKeys = new Set([
|
|
@@ -1024,7 +1031,8 @@ function renderRail() {
|
|
|
1024
1031
|
for (const key of projectEmployeeKeys) {
|
|
1025
1032
|
if (groups.has(key)) continue;
|
|
1026
1033
|
const p = typeof tfPersonaByKey === 'function' ? tfPersonaByKey(key) : null;
|
|
1027
|
-
|
|
1034
|
+
const availableToManager = !p || p.status === 'hired' || (p.seatCount || 0) > 0 || managerTeamKeySet.has(key);
|
|
1035
|
+
if (!availableToManager) continue;
|
|
1028
1036
|
groups.set(key, {
|
|
1029
1037
|
key,
|
|
1030
1038
|
label: p ? p.displayName : key,
|
|
@@ -6203,7 +6211,7 @@ const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
|
|
|
6203
6211
|
|
|
6204
6212
|
const tf = {
|
|
6205
6213
|
enabled: false,
|
|
6206
|
-
area: 'projects', // company | manager | projects | brain
|
|
6214
|
+
area: 'projects', // company | manager | projects | brain | connected
|
|
6207
6215
|
projectView: 'overview', // overview | workspace
|
|
6208
6216
|
activeProjectId: null,
|
|
6209
6217
|
projects: [], // [{ id, name, intent, brief }] — roster derived from hired personas, not stored
|
|
@@ -9234,16 +9242,107 @@ function tfToggleAccountMenu(e) {
|
|
|
9234
9242
|
const menu = document.getElementById('account-menu');
|
|
9235
9243
|
if (menu) menu.classList.toggle('open');
|
|
9236
9244
|
}
|
|
9237
|
-
function tfCloseAccountMenu() {
|
|
9238
|
-
const menu = document.getElementById('account-menu');
|
|
9239
|
-
if (menu) menu.classList.remove('open');
|
|
9240
|
-
}
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9245
|
+
function tfCloseAccountMenu() {
|
|
9246
|
+
const menu = document.getElementById('account-menu');
|
|
9247
|
+
if (menu) menu.classList.remove('open');
|
|
9248
|
+
}
|
|
9249
|
+
|
|
9250
|
+
const CONNECTED_SURFACES = {
|
|
9251
|
+
account: { title: 'Account', path: '/account/' },
|
|
9252
|
+
analytics: { title: 'Analytics', path: '/analytics/' },
|
|
9253
|
+
brain: { title: 'Brain', path: '/fraim-brain.html', redirectPath: '/fraim-brain' },
|
|
9254
|
+
};
|
|
9255
|
+
|
|
9256
|
+
function tfConnectedRemoteBase() {
|
|
9257
|
+
const fallback = window.location.origin;
|
|
9258
|
+
const raw = state.remoteBaseUrl || (state.bootstrap && state.bootstrap.remoteBaseUrl) || fallback;
|
|
9259
|
+
try {
|
|
9260
|
+
return new URL(raw, fallback).toString();
|
|
9261
|
+
} catch {
|
|
9262
|
+
return fallback;
|
|
9263
|
+
}
|
|
9264
|
+
}
|
|
9265
|
+
|
|
9266
|
+
function tfConnectedApiKey() {
|
|
9267
|
+
return state.storedApiKey || (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.apiKey) || '';
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9270
|
+
function tfConnectedSurfaceUrl(surface) {
|
|
9271
|
+
const cfg = CONNECTED_SURFACES[surface];
|
|
9272
|
+
const remoteBase = tfConnectedRemoteBase();
|
|
9273
|
+
if (!cfg) return remoteBase;
|
|
9274
|
+
const apiKey = tfConnectedApiKey();
|
|
9275
|
+
if (apiKey) {
|
|
9276
|
+
const target = new URL(cfg.path, remoteBase);
|
|
9277
|
+
target.searchParams.set('api-key', apiKey);
|
|
9278
|
+
return target.toString();
|
|
9279
|
+
}
|
|
9280
|
+
const signIn = new URL('/auth/sign-in.html', remoteBase);
|
|
9281
|
+
signIn.searchParams.set('surface', surface);
|
|
9282
|
+
signIn.searchParams.set('redirect_to', cfg.redirectPath || cfg.path);
|
|
9283
|
+
return signIn.toString();
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9286
|
+
function tfOpenConnectedSurface(surface) {
|
|
9287
|
+
const cfg = CONNECTED_SURFACES[surface];
|
|
9288
|
+
if (!cfg) return;
|
|
9289
|
+
tfCloseAccountMenu();
|
|
9290
|
+
state.connectedReturnArea = (tf.area && tf.area !== 'connected') ? tf.area : 'projects';
|
|
9291
|
+
const url = tfConnectedSurfaceUrl(surface);
|
|
9292
|
+
const title = document.getElementById('connected-title');
|
|
9293
|
+
const origin = document.getElementById('connected-origin');
|
|
9294
|
+
const frame = document.getElementById('connected-frame');
|
|
9295
|
+
if (title) title.textContent = cfg.title;
|
|
9296
|
+
if (origin) {
|
|
9297
|
+
try { origin.textContent = new URL(url).origin; }
|
|
9298
|
+
catch { origin.textContent = ''; }
|
|
9299
|
+
}
|
|
9300
|
+
if (frame) {
|
|
9301
|
+
frame.title = `FRAIM ${cfg.title}`;
|
|
9302
|
+
frame.src = url;
|
|
9303
|
+
}
|
|
9304
|
+
tfShowArea('connected');
|
|
9305
|
+
}
|
|
9306
|
+
|
|
9307
|
+
function tfCloseConnectedSurface() {
|
|
9308
|
+
const frame = document.getElementById('connected-frame');
|
|
9309
|
+
if (frame) frame.removeAttribute('src');
|
|
9310
|
+
const returnArea = state.connectedReturnArea && state.connectedReturnArea !== 'connected'
|
|
9311
|
+
? state.connectedReturnArea
|
|
9312
|
+
: 'projects';
|
|
9313
|
+
tfShowArea(returnArea);
|
|
9314
|
+
}
|
|
9315
|
+
|
|
9316
|
+
async function tfHandleConnectedAuthMessage(event) {
|
|
9317
|
+
const frame = document.getElementById('connected-frame');
|
|
9318
|
+
if (frame && frame.contentWindow && event.source !== frame.contentWindow) return;
|
|
9319
|
+
let remoteOrigin = '';
|
|
9320
|
+
try { remoteOrigin = new URL(tfConnectedRemoteBase()).origin; }
|
|
9321
|
+
catch { return; }
|
|
9322
|
+
if (event.origin !== remoteOrigin) return;
|
|
9323
|
+
const msg = event.data || {};
|
|
9324
|
+
if (!msg || msg.type !== 'fraim-auth-api-key' || typeof msg.apiKey !== 'string' || !msg.apiKey) return;
|
|
9325
|
+
if (msg.apiKey === state.storedApiKey) return;
|
|
9326
|
+
state.storedApiKey = msg.apiKey;
|
|
9327
|
+
if (state.bootstrap && state.bootstrap.preferences) {
|
|
9328
|
+
state.bootstrap.preferences.apiKey = msg.apiKey;
|
|
9329
|
+
}
|
|
9330
|
+
try {
|
|
9331
|
+
await requestJson('/api/ai-hub/api-key', {
|
|
9332
|
+
method: 'POST',
|
|
9333
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9334
|
+
body: JSON.stringify({ apiKey: msg.apiKey }),
|
|
9335
|
+
});
|
|
9336
|
+
} catch (error) {
|
|
9337
|
+
console.warn('[ai-hub] Could not persist connected auth key:', error);
|
|
9338
|
+
}
|
|
9339
|
+
}
|
|
9340
|
+
|
|
9341
|
+
// ---------------------------------------------------------------------------
|
|
9342
|
+
// Theme (#700): explicit light/dark with an in-app toggle. The <head> bootstrap
|
|
9244
9343
|
// sets [data-theme] pre-paint (no flash) from localStorage['fraim-theme'] or the
|
|
9245
9344
|
// OS preference; here we reflect it in the toggle control and persist changes.
|
|
9246
|
-
//
|
|
9345
|
+
// Embedded connected surfaces receive live theme updates via postMessage.
|
|
9247
9346
|
// ---------------------------------------------------------------------------
|
|
9248
9347
|
function tfCurrentTheme() {
|
|
9249
9348
|
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
@@ -9263,13 +9362,18 @@ function tfApplyTheme(theme, persist) {
|
|
|
9263
9362
|
document.documentElement.setAttribute('data-theme', t);
|
|
9264
9363
|
if (persist) { try { localStorage.setItem('fraim-theme', t); } catch (e) {} }
|
|
9265
9364
|
tfSyncThemeToggle(t);
|
|
9266
|
-
// Keep
|
|
9267
|
-
try {
|
|
9268
|
-
document.querySelectorAll('iframe').forEach((f) => {
|
|
9269
|
-
if (f.contentWindow)
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9365
|
+
// Keep embedded surfaces in sync live, including the connected main-server frame.
|
|
9366
|
+
try {
|
|
9367
|
+
document.querySelectorAll('iframe').forEach((f) => {
|
|
9368
|
+
if (!f.contentWindow) return;
|
|
9369
|
+
let targetOrigin = location.origin;
|
|
9370
|
+
try {
|
|
9371
|
+
if (f.src) targetOrigin = new URL(f.src, location.href).origin;
|
|
9372
|
+
} catch (e) {}
|
|
9373
|
+
f.contentWindow.postMessage({ type: 'fraim-theme-change', theme: t }, targetOrigin);
|
|
9374
|
+
});
|
|
9375
|
+
} catch (e) {}
|
|
9376
|
+
}
|
|
9273
9377
|
function tfToggleTheme(e) {
|
|
9274
9378
|
if (e) e.stopPropagation();
|
|
9275
9379
|
tfApplyTheme(tfCurrentTheme() === 'dark' ? 'light' : 'dark', true);
|
|
@@ -9995,13 +10099,31 @@ function tfWireShell() {
|
|
|
9995
10099
|
for (const tab of document.querySelectorAll('.hub-tab')) {
|
|
9996
10100
|
tab.addEventListener('click', () => tfShowArea(tab.dataset.area));
|
|
9997
10101
|
}
|
|
9998
|
-
const avatar = document.getElementById('avatar-btn');
|
|
9999
|
-
if (avatar) avatar.addEventListener('click', tfToggleAccountMenu);
|
|
10000
|
-
tfWireThemeToggle();
|
|
10001
|
-
const
|
|
10002
|
-
if (
|
|
10003
|
-
|
|
10004
|
-
|
|
10102
|
+
const avatar = document.getElementById('avatar-btn');
|
|
10103
|
+
if (avatar) avatar.addEventListener('click', tfToggleAccountMenu);
|
|
10104
|
+
tfWireThemeToggle();
|
|
10105
|
+
const accountItem = document.getElementById('am-account');
|
|
10106
|
+
if (accountItem) accountItem.addEventListener('click', (e) => {
|
|
10107
|
+
e.preventDefault();
|
|
10108
|
+
tfOpenConnectedSurface('account');
|
|
10109
|
+
});
|
|
10110
|
+
const brainItem = document.getElementById('am-brain');
|
|
10111
|
+
if (brainItem) brainItem.addEventListener('click', () => {
|
|
10112
|
+
tfOpenConnectedSurface('brain');
|
|
10113
|
+
});
|
|
10114
|
+
const analyticsItem = document.getElementById('am-analytics');
|
|
10115
|
+
if (analyticsItem) analyticsItem.addEventListener('click', (e) => {
|
|
10116
|
+
e.preventDefault();
|
|
10117
|
+
tfOpenConnectedSurface('analytics');
|
|
10118
|
+
});
|
|
10119
|
+
const connectedClose = document.getElementById('connected-close');
|
|
10120
|
+
if (connectedClose) connectedClose.addEventListener('click', tfCloseConnectedSurface);
|
|
10121
|
+
if (!tf.connectedAuthMessageWired) {
|
|
10122
|
+
window.addEventListener('message', tfHandleConnectedAuthMessage);
|
|
10123
|
+
tf.connectedAuthMessageWired = true;
|
|
10124
|
+
}
|
|
10125
|
+
const signout = document.getElementById('am-signout');
|
|
10126
|
+
if (signout) signout.addEventListener('click', () => { window.location.href = '/auth/sign-in.html'; });
|
|
10005
10127
|
document.addEventListener('click', (e) => {
|
|
10006
10128
|
const menu = document.getElementById('account-menu');
|
|
10007
10129
|
if (menu && menu.classList.contains('open') && !(e.target.closest && e.target.closest('.nav-right'))) {
|
|
@@ -10109,10 +10231,15 @@ function tfWireShell() {
|
|
|
10109
10231
|
['assign-job-modal', tfCloseAssignJob],
|
|
10110
10232
|
['np-modal', tfCloseNewProject],
|
|
10111
10233
|
];
|
|
10112
|
-
document.addEventListener('keydown', (e) => {
|
|
10113
|
-
if (e.key !== 'Escape') return;
|
|
10114
|
-
|
|
10115
|
-
|
|
10234
|
+
document.addEventListener('keydown', (e) => {
|
|
10235
|
+
if (e.key !== 'Escape') return;
|
|
10236
|
+
if (tf.area === 'connected') {
|
|
10237
|
+
e.stopPropagation();
|
|
10238
|
+
tfCloseConnectedSurface();
|
|
10239
|
+
return;
|
|
10240
|
+
}
|
|
10241
|
+
for (const [id, fn] of escClosers) {
|
|
10242
|
+
const el = document.getElementById(id);
|
|
10116
10243
|
if (el && !el.hidden) { e.stopPropagation(); fn(); return; }
|
|
10117
10244
|
}
|
|
10118
10245
|
});
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -2752,7 +2752,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2752
2752
|
.am-av { width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
|
|
2753
2753
|
.am-name { font-size: 13px; font-weight: 700; }
|
|
2754
2754
|
.am-email { font-size: 11px; color: var(--muted); }
|
|
2755
|
-
.am-item { display: flex; align-items: flex-start; gap: 10px; padding: 9px 16px; cursor: pointer; font-size: 13px; }
|
|
2755
|
+
.am-item { display: flex; align-items: flex-start; gap: 10px; width: 100%; padding: 9px 16px; cursor: pointer; font-size: 13px; color: var(--text); text-align: left; text-decoration: none; border: 0; background: none; }
|
|
2756
2756
|
.am-item:hover { background: var(--bg); }
|
|
2757
2757
|
.am-ico { font-size: 14px; width: 18px; text-align: center; flex-shrink: 0; margin-top: 1px; }
|
|
2758
2758
|
.am-label { font-weight: 600; }
|
|
@@ -2768,12 +2768,75 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2768
2768
|
.am-theme[aria-checked="true"] .am-switch-knob { transform: translateX(16px); }
|
|
2769
2769
|
|
|
2770
2770
|
/* Hub areas */
|
|
2771
|
-
.hub-area { display: none; flex: 1; overflow: hidden; }
|
|
2772
|
-
.hub-area.on { display: flex; flex-direction: column; }
|
|
2773
|
-
.hub-area-page { max-width: 860px; margin: 0 auto; padding: 28px 24px 48px; overflow-y: auto; flex: 1; /* authoritative rule — see also Round-2 override below which is now removed */ }
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2771
|
+
.hub-area { display: none; flex: 1; overflow: hidden; }
|
|
2772
|
+
.hub-area.on { display: flex; flex-direction: column; }
|
|
2773
|
+
.hub-area-page { max-width: 860px; margin: 0 auto; padding: 28px 24px 48px; overflow-y: auto; flex: 1; /* authoritative rule — see also Round-2 override below which is now removed */ }
|
|
2774
|
+
|
|
2775
|
+
.connected-shell {
|
|
2776
|
+
display: flex;
|
|
2777
|
+
flex: 1;
|
|
2778
|
+
min-height: 0;
|
|
2779
|
+
flex-direction: column;
|
|
2780
|
+
background: var(--bg);
|
|
2781
|
+
}
|
|
2782
|
+
.connected-toolbar {
|
|
2783
|
+
display: flex;
|
|
2784
|
+
align-items: center;
|
|
2785
|
+
gap: 14px;
|
|
2786
|
+
min-height: 52px;
|
|
2787
|
+
padding: 8px 16px;
|
|
2788
|
+
background: var(--surface);
|
|
2789
|
+
border-bottom: 1px solid var(--line);
|
|
2790
|
+
flex-shrink: 0;
|
|
2791
|
+
}
|
|
2792
|
+
.connected-title-block {
|
|
2793
|
+
min-width: 0;
|
|
2794
|
+
display: flex;
|
|
2795
|
+
flex-direction: column;
|
|
2796
|
+
gap: 1px;
|
|
2797
|
+
}
|
|
2798
|
+
.connected-kicker {
|
|
2799
|
+
font-size: 10px;
|
|
2800
|
+
font-weight: 700;
|
|
2801
|
+
letter-spacing: .04em;
|
|
2802
|
+
text-transform: uppercase;
|
|
2803
|
+
color: var(--muted);
|
|
2804
|
+
}
|
|
2805
|
+
.connected-title {
|
|
2806
|
+
font-size: 14px;
|
|
2807
|
+
font-weight: 700;
|
|
2808
|
+
color: var(--text);
|
|
2809
|
+
}
|
|
2810
|
+
.connected-origin {
|
|
2811
|
+
min-width: 0;
|
|
2812
|
+
flex: 1;
|
|
2813
|
+
color: var(--muted);
|
|
2814
|
+
font-size: 12px;
|
|
2815
|
+
overflow: hidden;
|
|
2816
|
+
text-overflow: ellipsis;
|
|
2817
|
+
white-space: nowrap;
|
|
2818
|
+
}
|
|
2819
|
+
.connected-close {
|
|
2820
|
+
border: 1px solid var(--line);
|
|
2821
|
+
background: var(--surface);
|
|
2822
|
+
color: var(--text);
|
|
2823
|
+
border-radius: 8px;
|
|
2824
|
+
padding: 6px 12px;
|
|
2825
|
+
font-size: 13px;
|
|
2826
|
+
font-weight: 600;
|
|
2827
|
+
flex-shrink: 0;
|
|
2828
|
+
}
|
|
2829
|
+
.connected-close:hover { background: var(--bg); }
|
|
2830
|
+
.connected-frame {
|
|
2831
|
+
flex: 1;
|
|
2832
|
+
min-height: 0;
|
|
2833
|
+
width: 100%;
|
|
2834
|
+
border: 0;
|
|
2835
|
+
background: var(--surface);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
/* #521 R6: Company/Manager use the same rail + main shell as the project workspace. */
|
|
2839
|
+
.area-shell { display: flex; flex: 1; min-height: 0; }
|
|
2777
2840
|
.area-rail { width: 244px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--line); overflow-y: auto; padding: 6px 0; }
|
|
2778
2841
|
.area-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
2779
2842
|
.area-rail-head { font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding: 14px 16px 6px; }
|