fraim 2.0.159 → 2.0.161
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/README.md +1 -1
- package/dist/src/ai-hub/cert-store.js +70 -0
- package/dist/src/ai-hub/desktop-main.js +224 -50
- package/dist/src/ai-hub/manager-turns.js +38 -0
- package/dist/src/ai-hub/office-sideload.js +156 -0
- package/dist/src/ai-hub/openclaw-bridge.js +239 -0
- package/dist/src/ai-hub/server.js +362 -115
- package/dist/src/ai-hub/word-sideload.js +95 -0
- package/dist/src/cli/commands/add-ide.js +9 -0
- package/dist/src/cli/commands/login.js +1 -2
- package/dist/src/cli/commands/setup.js +0 -2
- package/dist/src/cli/commands/sync.js +19 -10
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +66 -2
- package/dist/src/cli/doctor/checks/workflow-checks.js +1 -65
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +14 -10
- package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
- package/dist/src/cli/utils/fraim-gitignore.js +11 -0
- package/dist/src/cli/utils/remote-sync.js +1 -1
- package/dist/src/core/config-loader.js +1 -2
- package/dist/src/core/fraim-config-schema.generated.js +0 -5
- package/dist/src/core/quality-evidence.js +4 -1
- package/dist/src/core/types.js +0 -1
- package/dist/src/first-run/session-service.js +3 -3
- package/dist/src/local-mcp-server/stdio-server.js +28 -9
- package/package.json +2 -1
- package/public/ai-hub/index.html +20 -2
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -0
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
- package/public/ai-hub/script.js +337 -120
- package/public/ai-hub/styles.css +456 -135
|
@@ -8,10 +8,6 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = exports.FRAIM_CONFIG_SCHEMA = void 0;
|
|
|
8
8
|
exports.FRAIM_CONFIG_SCHEMA = {
|
|
9
9
|
"kind": "object",
|
|
10
10
|
"properties": {
|
|
11
|
-
"version": {
|
|
12
|
-
"kind": "string",
|
|
13
|
-
"required": true
|
|
14
|
-
},
|
|
15
11
|
"project": {
|
|
16
12
|
"kind": "object",
|
|
17
13
|
"properties": {
|
|
@@ -487,7 +483,6 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
487
483
|
"required": true
|
|
488
484
|
};
|
|
489
485
|
exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
490
|
-
"version",
|
|
491
486
|
"project",
|
|
492
487
|
"project.name",
|
|
493
488
|
"project.industry",
|
|
@@ -267,6 +267,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
267
267
|
? [
|
|
268
268
|
'```javascript',
|
|
269
269
|
'evidence: {',
|
|
270
|
+
' artifactPath: "<project-relative quality artifact path>",',
|
|
270
271
|
' quality: {',
|
|
271
272
|
' gateDecision: "<pass|flag|fail>",',
|
|
272
273
|
' interviewsAnalyzed: <number>,',
|
|
@@ -281,6 +282,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
281
282
|
? [
|
|
282
283
|
'```javascript',
|
|
283
284
|
'evidence: {',
|
|
285
|
+
' artifactPath: "<project-relative quality artifact path>",',
|
|
284
286
|
' quality: {',
|
|
285
287
|
' composite: <number 0-10>,',
|
|
286
288
|
' participant: { fit: <number 1-10>, urgency: <number 1-10>, authority: <number 1-10> },',
|
|
@@ -298,6 +300,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
298
300
|
: [
|
|
299
301
|
'```javascript',
|
|
300
302
|
'evidence: {',
|
|
303
|
+
' artifactPath: "<project-relative quality artifact path>",',
|
|
301
304
|
' quality: {',
|
|
302
305
|
' composite: <number 0-10>,',
|
|
303
306
|
' coaching: "<actionable recommendation>",',
|
|
@@ -315,7 +318,7 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
315
318
|
return [
|
|
316
319
|
`❌ **Job completion rejected** for \`${jobName}\`.`,
|
|
317
320
|
'',
|
|
318
|
-
`This job is required to emit
|
|
321
|
+
`This job is required to emit quality evidence on its final \`seekMentoring\` call so the result is captured in \`fraim_quality_scores\` for the quality dashboard. The following problems were found:`,
|
|
319
322
|
'',
|
|
320
323
|
errorBullets,
|
|
321
324
|
...importantNote,
|
package/dist/src/core/types.js
CHANGED
|
@@ -507,7 +507,7 @@ class FirstRunSessionService {
|
|
|
507
507
|
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
508
508
|
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
509
509
|
this.persist();
|
|
510
|
-
await runProcess('npm', ['install', '-g', 'fraim
|
|
510
|
+
await runProcess('npm', ['install', '-g', 'fraim@latest'], { npm_config_prefix: prefix });
|
|
511
511
|
}
|
|
512
512
|
persistShellPath();
|
|
513
513
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
@@ -752,7 +752,7 @@ class FirstRunSessionService {
|
|
|
752
752
|
appendInstallLog(`hub-opened ${hubUrl}`);
|
|
753
753
|
return {
|
|
754
754
|
ok: true,
|
|
755
|
-
message: `Hub is open at ${hubUrl}. From now on, run \`npx fraim
|
|
755
|
+
message: `Hub is open at ${hubUrl}. From now on, run \`npx fraim@latest hub --browser\` to launch it again — the standalone launcher binary ships in v2 (#355).`,
|
|
756
756
|
hubUrl,
|
|
757
757
|
};
|
|
758
758
|
}
|
|
@@ -761,7 +761,7 @@ class FirstRunSessionService {
|
|
|
761
761
|
appendInstallLog(`hub-open-failed ${detail}`);
|
|
762
762
|
return {
|
|
763
763
|
ok: false,
|
|
764
|
-
message: `Could not open the Hub automatically: ${detail}. Run \`npx fraim
|
|
764
|
+
message: `Could not open the Hub automatically: ${detail}. Run \`npx fraim@latest hub --browser\` from a terminal to open it manually.`,
|
|
765
765
|
};
|
|
766
766
|
}
|
|
767
767
|
}
|
|
@@ -663,14 +663,28 @@ class FraimLocalMCPServer {
|
|
|
663
663
|
}
|
|
664
664
|
async performLocalCatalogSync(projectRoot) {
|
|
665
665
|
const { runSync } = await Promise.resolve().then(() => __importStar(require('../cli/commands/sync')));
|
|
666
|
-
await
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
666
|
+
await this.runWithConsoleLogRedirectedToStderr(async () => {
|
|
667
|
+
await runSync({
|
|
668
|
+
projectRoot,
|
|
669
|
+
skipUpdates: true,
|
|
670
|
+
local: this.shouldUseLocalSyncTarget(),
|
|
671
|
+
failHard: 'throw'
|
|
672
|
+
});
|
|
671
673
|
});
|
|
672
674
|
this.writeLocalCatalogMetadata(projectRoot);
|
|
673
675
|
}
|
|
676
|
+
async runWithConsoleLogRedirectedToStderr(operation) {
|
|
677
|
+
const originalLog = console.log;
|
|
678
|
+
console.log = (...args) => {
|
|
679
|
+
console.error(...args);
|
|
680
|
+
};
|
|
681
|
+
try {
|
|
682
|
+
return await operation();
|
|
683
|
+
}
|
|
684
|
+
finally {
|
|
685
|
+
console.log = originalLog;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
674
688
|
async ensureFreshLocalCatalogOnConnect(requestId) {
|
|
675
689
|
if (process.env.FRAIM_DISABLE_SYNC_ON_CONNECT === '1') {
|
|
676
690
|
this.latestConnectSyncWarning = null;
|
|
@@ -1863,7 +1877,7 @@ class FraimLocalMCPServer {
|
|
|
1863
1877
|
//
|
|
1864
1878
|
// When a QUALITY_PRODUCING_JOBS job reaches its final phase
|
|
1865
1879
|
// (nextPhase === null) with status === 'complete', the agent MUST
|
|
1866
|
-
// include a valid `evidence.quality` object. If invalid, swap the
|
|
1880
|
+
// include `evidence.artifactPath` and a valid `evidence.quality` object. If invalid, swap the
|
|
1867
1881
|
// accomplishment message for a rejection and DO NOT emit. If valid,
|
|
1868
1882
|
// fire-and-forget POST to /api/analytics/quality-score so the row
|
|
1869
1883
|
// lands in fraim_quality_scores.
|
|
@@ -1871,8 +1885,13 @@ class FraimLocalMCPServer {
|
|
|
1871
1885
|
const isFinalCompletion = args.status === 'complete' &&
|
|
1872
1886
|
(tutoringResponse.nextPhase === null || tutoringResponse.nextPhase === undefined);
|
|
1873
1887
|
if (isQualityJob && isFinalCompletion) {
|
|
1874
|
-
const qualityErrors =
|
|
1875
|
-
|
|
1888
|
+
const qualityErrors = [
|
|
1889
|
+
...((0, quality_evidence_1.validateQualityEvidence)(args.evidence?.quality, args.jobName) || []),
|
|
1890
|
+
...(typeof args.evidence?.artifactPath === 'string' && args.evidence.artifactPath.trim()
|
|
1891
|
+
? []
|
|
1892
|
+
: ['evidence.artifactPath is required'])
|
|
1893
|
+
];
|
|
1894
|
+
if (qualityErrors.length > 0) {
|
|
1876
1895
|
this.log(`❌ Quality enforcement rejected ${args.jobName} completion: ${qualityErrors.join('; ')}`);
|
|
1877
1896
|
const rejection = (0, quality_evidence_1.buildQualityRejectionMessage)(args.jobName, args.currentPhase, qualityErrors);
|
|
1878
1897
|
return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
|
|
@@ -1886,7 +1905,7 @@ class FraimLocalMCPServer {
|
|
|
1886
1905
|
jobId: args.jobId,
|
|
1887
1906
|
sessionId: requestSessionId || args.sessionId || 'unknown',
|
|
1888
1907
|
quality: args.evidence.quality,
|
|
1889
|
-
artifactPath: args.evidence
|
|
1908
|
+
artifactPath: args.evidence?.artifactPath,
|
|
1890
1909
|
repoIdentifier: args.evidence?.reviewContext?.repoIdentifier || this.repoInfo?.url,
|
|
1891
1910
|
reviewContext: {
|
|
1892
1911
|
...(args.evidence?.reviewContext || {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.161",
|
|
4
4
|
"description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
"nodemailer": "^8.0.3",
|
|
152
152
|
"prompts": "^2.4.2",
|
|
153
153
|
"resend": "^6.9.3",
|
|
154
|
+
"selfsigned": "^5.5.0",
|
|
154
155
|
"semver": "^7.7.4",
|
|
155
156
|
"stripe": "^20.3.1",
|
|
156
157
|
"toml": "^3.0.0",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>FRAIM AI Hub</title>
|
|
7
8
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='10' fill='%233d8a6e'/%3E%3Ctext x='24' y='32' text-anchor='middle' font-family='Helvetica,Arial,sans-serif' font-size='22' font-weight='700' fill='white'%3EH%3C/text%3E%3C/svg%3E">
|
|
9
|
+
<!-- Detect Electron before CSS paints so body.electron rules apply on first render -->
|
|
10
|
+
<script>if(/Electron/.test(navigator.userAgent)){document.documentElement.classList.add('electron');}</script>
|
|
8
11
|
<link rel="stylesheet" href="./styles.css">
|
|
9
12
|
</head>
|
|
10
13
|
<body>
|
|
11
14
|
|
|
12
15
|
<div class="page">
|
|
13
16
|
|
|
14
|
-
<header class="header">
|
|
17
|
+
<header class="header" style="-webkit-app-region: drag;">
|
|
15
18
|
<h1 class="header-title">Manage your FRAIM Employees</h1>
|
|
16
19
|
<section class="welcome">
|
|
17
20
|
Hi <strong class="you">there</strong>, remember, you are the
|
|
@@ -63,6 +66,7 @@
|
|
|
63
66
|
|
|
64
67
|
<div class="layout">
|
|
65
68
|
<aside class="rail">
|
|
69
|
+
<span id="tp-project-label" class="tp-project-label"></span>
|
|
66
70
|
<button class="new-conv" type="button" id="new-conv-btn">+ New job</button>
|
|
67
71
|
<div class="rail-note">Alpha: browser shell for directing employees across your project.</div>
|
|
68
72
|
<section class="rail-section rail-section--employees">
|
|
@@ -76,6 +80,12 @@
|
|
|
76
80
|
</section>
|
|
77
81
|
</aside>
|
|
78
82
|
|
|
83
|
+
<div id="word-context-bar" class="word-context-bar" hidden>
|
|
84
|
+
<span class="word-ctx-icon">📄</span>
|
|
85
|
+
<span class="word-ctx-text" id="word-ctx-text"></span>
|
|
86
|
+
<button class="word-ctx-refresh" type="button" id="word-ctx-refresh" title="Refresh document context">↻</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
79
89
|
<main class="conversation" id="conversation">
|
|
80
90
|
<div class="empty-state" id="empty">
|
|
81
91
|
<h3>No job selected</h3>
|
|
@@ -241,6 +251,14 @@
|
|
|
241
251
|
<div class="desc" id="picked-desc"></div>
|
|
242
252
|
</div>
|
|
243
253
|
<textarea id="instructions" placeholder="What outcome do you want? Any context the employee should know?"></textarea>
|
|
254
|
+
<div id="word-context-card" class="word-context-card" hidden>
|
|
255
|
+
<div class="word-ctx-card-header">
|
|
256
|
+
<span class="word-ctx-card-icon">📄</span>
|
|
257
|
+
<span class="word-ctx-card-label" id="word-ctx-card-label">Document context</span>
|
|
258
|
+
<button type="button" class="word-ctx-card-toggle" id="word-ctx-card-toggle" aria-label="Expand document context">▸</button>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="word-ctx-card-body" id="word-ctx-card-body" hidden></div>
|
|
261
|
+
</div>
|
|
244
262
|
<div class="employee-line">
|
|
245
263
|
<span class="employee-label">Employee:</span>
|
|
246
264
|
<select id="employee-select" class="employee-select"></select>
|
|
Binary file
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FRAIM Hub</title>
|
|
7
|
+
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript"></script>
|
|
8
|
+
<script src="config.js" type="text/javascript"></script>
|
|
9
|
+
<style>
|
|
10
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
+
html, body { height: 100%; overflow: hidden; }
|
|
12
|
+
iframe { width: 100%; height: 100vh; border: none; display: block; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<iframe id="hub" src="" allow="clipboard-read; clipboard-write"></iframe>
|
|
17
|
+
<script>
|
|
18
|
+
// PowerPoint task pane — mirrors extensions/office-word/taskpane.html so PPT
|
|
19
|
+
// gets the identical full Hub experience. It mounts the Hub /ai-hub/ surface in
|
|
20
|
+
// an iframe and bridges host context over postMessage using the same
|
|
21
|
+
// word-context / word-request / word-response contract the Hub already speaks
|
|
22
|
+
// (the names are the generic host-bridge protocol, not Word-specific).
|
|
23
|
+
//
|
|
24
|
+
// In Office Online the pane is served over HTTPS; loading an HTTP iframe from an
|
|
25
|
+
// HTTPS page is mixed-content-blocked, so use the pane's own origin there. On
|
|
26
|
+
// desktop (HTTP) use the direct Hub address.
|
|
27
|
+
var HUB_ORIGIN = window.location.protocol === 'https:'
|
|
28
|
+
? window.location.origin
|
|
29
|
+
: (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
|
|
30
|
+
var hubFrame = document.getElementById('hub');
|
|
31
|
+
var pendingPush = null; // context queued before hub-ready fires
|
|
32
|
+
var hubReady = false;
|
|
33
|
+
var selectionHandlerAdded = false;
|
|
34
|
+
|
|
35
|
+
// ── postMessage bridge (inbound from Hub) ─────────────────────────────────
|
|
36
|
+
window.addEventListener('message', function(event) {
|
|
37
|
+
if (event.origin !== HUB_ORIGIN) return;
|
|
38
|
+
var msg = event.data || {};
|
|
39
|
+
if (msg.type === 'hub-ready') {
|
|
40
|
+
hubReady = true;
|
|
41
|
+
if (pendingPush) { pushToHub(pendingPush); pendingPush = null; }
|
|
42
|
+
} else if (msg.type === 'word-request') {
|
|
43
|
+
handleHostRequest(msg.action, msg.requestId, msg.payload || {});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function pushToHub(msg) {
|
|
48
|
+
if (hubFrame && hubFrame.contentWindow) {
|
|
49
|
+
hubFrame.contentWindow.postMessage(msg, HUB_ORIGIN);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Reading PowerPoint context ──────────────────────────────────────────────
|
|
54
|
+
function docMeta() {
|
|
55
|
+
var meta = { docUrl: '', docTitle: '' };
|
|
56
|
+
try {
|
|
57
|
+
var doc = window.Office && Office.context && Office.context.document;
|
|
58
|
+
meta.docUrl = (doc && doc.url) ? doc.url : '';
|
|
59
|
+
if (meta.docUrl) {
|
|
60
|
+
var parts = meta.docUrl.replace(/\\/g, '/').split('/');
|
|
61
|
+
meta.docTitle = (parts[parts.length - 1] || '').replace(/\.[^.]+$/, '');
|
|
62
|
+
}
|
|
63
|
+
} catch(e) {}
|
|
64
|
+
return meta;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readSelectionAsync(cb) {
|
|
68
|
+
try {
|
|
69
|
+
Office.context.document.getSelectedDataAsync(
|
|
70
|
+
Office.CoercionType.Text,
|
|
71
|
+
function(r) { cb(r.status === Office.AsyncResultStatus.Succeeded ? String(r.value || '').trim() : ''); }
|
|
72
|
+
);
|
|
73
|
+
} catch(e) { cb(''); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read the current slide's text + speaker notes via the PowerPoint API.
|
|
77
|
+
function readSlideAsync(cb) {
|
|
78
|
+
try {
|
|
79
|
+
if (typeof PowerPoint !== 'undefined' && PowerPoint.run) {
|
|
80
|
+
PowerPoint.run(function(ctx) {
|
|
81
|
+
var slides = ctx.presentation.getSelectedSlides();
|
|
82
|
+
slides.load('items');
|
|
83
|
+
return ctx.sync().then(function() {
|
|
84
|
+
var slide = slides.items && slides.items[0];
|
|
85
|
+
if (!slide) { cb({ slideText: '', notes: '' }); return; }
|
|
86
|
+
var shapes = slide.shapes;
|
|
87
|
+
shapes.load('items/textFrame/textRange/text');
|
|
88
|
+
var ns = slide.notesSlide;
|
|
89
|
+
try { ns.load('notesTextFrame/textRange/text'); } catch(e) {}
|
|
90
|
+
return ctx.sync().then(function() {
|
|
91
|
+
var notes = '';
|
|
92
|
+
try { notes = ns.notesTextFrame.textRange.text || ''; } catch(e) {}
|
|
93
|
+
var slideText = (shapes.items || [])
|
|
94
|
+
.map(function(s){ try { return s.textFrame.textRange.text || ''; } catch(e){ return ''; } })
|
|
95
|
+
.filter(Boolean).join('\n');
|
|
96
|
+
cb({ slideText: slideText, notes: notes });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}).catch(function() { cb({ slideText: '', notes: '' }); });
|
|
100
|
+
} else { cb({ slideText: '', notes: '' }); }
|
|
101
|
+
} catch(e) { cb({ slideText: '', notes: '' }); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function slideCountAsync(cb) {
|
|
105
|
+
try {
|
|
106
|
+
if (typeof PowerPoint !== 'undefined' && PowerPoint.run) {
|
|
107
|
+
PowerPoint.run(function(ctx) {
|
|
108
|
+
var slides = ctx.presentation.slides;
|
|
109
|
+
slides.load('items');
|
|
110
|
+
return ctx.sync().then(function() { cb((slides.items || []).length); });
|
|
111
|
+
}).catch(function() { cb(0); });
|
|
112
|
+
} else { cb(0); }
|
|
113
|
+
} catch(e) { cb(0); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readFullContext(cb) {
|
|
117
|
+
var meta = docMeta();
|
|
118
|
+
readSelectionAsync(function(selection) {
|
|
119
|
+
readSlideAsync(function(slide) {
|
|
120
|
+
slideCountAsync(function(count) {
|
|
121
|
+
var body = [slide.slideText, slide.notes ? ('Notes: ' + slide.notes) : ''].filter(Boolean).join('\n');
|
|
122
|
+
cb({
|
|
123
|
+
docUrl: meta.docUrl,
|
|
124
|
+
docTitle: meta.docTitle,
|
|
125
|
+
selection: selection,
|
|
126
|
+
hasSelection: selection.length > 0,
|
|
127
|
+
bodyPreview: body.slice(0, 2000),
|
|
128
|
+
wordCount: body ? body.split(/\s+/).filter(Boolean).length : 0,
|
|
129
|
+
slideCount: count,
|
|
130
|
+
comments: [],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Selection-change handler ───────────────────────────────────────────────
|
|
138
|
+
function addSelectionHandler() {
|
|
139
|
+
if (selectionHandlerAdded) return;
|
|
140
|
+
try {
|
|
141
|
+
Office.context.document.addHandlerAsync(
|
|
142
|
+
Office.EventType.DocumentSelectionChanged,
|
|
143
|
+
function() {
|
|
144
|
+
readSelectionAsync(function(sel) {
|
|
145
|
+
var m = docMeta();
|
|
146
|
+
pushToHub({ type: 'word-context-update', payload: {
|
|
147
|
+
docUrl: m.docUrl, docTitle: m.docTitle,
|
|
148
|
+
selection: sel, hasSelection: sel.length > 0,
|
|
149
|
+
}});
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
function(r) { if (r.status === Office.AsyncResultStatus.Succeeded) selectionHandlerAdded = true; }
|
|
153
|
+
);
|
|
154
|
+
} catch(e) {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Handling requests from Hub ─────────────────────────────────────────────
|
|
158
|
+
function handleHostRequest(action, requestId, payload) {
|
|
159
|
+
function respond(result) {
|
|
160
|
+
pushToHub({ type: 'word-response', requestId: requestId, payload: result });
|
|
161
|
+
}
|
|
162
|
+
if (action === 'get-context') {
|
|
163
|
+
readFullContext(function(ctx) { respond(ctx); });
|
|
164
|
+
} else if (action === 'get-selection') {
|
|
165
|
+
readSelectionAsync(function(sel) { respond({ selection: sel, hasSelection: sel.length > 0 }); });
|
|
166
|
+
} else if (action === 'insert-text') {
|
|
167
|
+
// Insert a new text box on the current slide with the given text.
|
|
168
|
+
try {
|
|
169
|
+
if (typeof PowerPoint !== 'undefined' && PowerPoint.run) {
|
|
170
|
+
PowerPoint.run(function(ctx) {
|
|
171
|
+
var slides = ctx.presentation.getSelectedSlides();
|
|
172
|
+
slides.load('items');
|
|
173
|
+
return ctx.sync().then(function() {
|
|
174
|
+
var slide = slides.items && slides.items[0];
|
|
175
|
+
if (!slide) throw new Error('No slide selected');
|
|
176
|
+
var box = slide.shapes.addTextBox(payload.text || '');
|
|
177
|
+
box.left = 50; box.top = 50; box.width = 600; box.height = 100;
|
|
178
|
+
return ctx.sync();
|
|
179
|
+
});
|
|
180
|
+
}).then(function() { respond({ ok: true }); }).catch(function(e) { respond({ ok: false, error: e.message }); });
|
|
181
|
+
} else { respond({ ok: false, error: 'PowerPoint API 1.1+ not available' }); }
|
|
182
|
+
} catch(e) { respond({ ok: false, error: e.message }); }
|
|
183
|
+
} else if (action === 'insert-after' || action === 'append-to-doc') {
|
|
184
|
+
// Append text into the current slide's speaker notes.
|
|
185
|
+
try {
|
|
186
|
+
if (typeof PowerPoint !== 'undefined' && PowerPoint.run) {
|
|
187
|
+
PowerPoint.run(function(ctx) {
|
|
188
|
+
var slides = ctx.presentation.getSelectedSlides();
|
|
189
|
+
slides.load('items');
|
|
190
|
+
return ctx.sync().then(function() {
|
|
191
|
+
var slide = slides.items && slides.items[0];
|
|
192
|
+
if (!slide) throw new Error('No slide selected');
|
|
193
|
+
var ns = slide.notesSlide;
|
|
194
|
+
ns.load('notesTextFrame/textRange/text');
|
|
195
|
+
return ctx.sync().then(function() {
|
|
196
|
+
var existing = '';
|
|
197
|
+
try { existing = ns.notesTextFrame.textRange.text || ''; } catch(e) {}
|
|
198
|
+
ns.notesTextFrame.textRange.text = (existing ? existing + '\n' : '') + (payload.text || '');
|
|
199
|
+
return ctx.sync();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}).then(function() { respond({ ok: true }); }).catch(function(e) { respond({ ok: false, error: e.message }); });
|
|
203
|
+
} else { respond({ ok: false, error: 'PowerPoint API not available' }); }
|
|
204
|
+
} catch(e) { respond({ ok: false, error: e.message }); }
|
|
205
|
+
} else {
|
|
206
|
+
respond({ ok: false, error: 'Unknown action: ' + action });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Mount ─────────────────────────────────────────────────────────────────
|
|
211
|
+
function mountHub() {
|
|
212
|
+
var meta = docMeta();
|
|
213
|
+
var params = new URLSearchParams({ surface: 'task-pane' });
|
|
214
|
+
if (meta.docUrl) params.set('docUrl', meta.docUrl);
|
|
215
|
+
if (meta.docTitle) params.set('docTitle', meta.docTitle);
|
|
216
|
+
hubFrame.src = HUB_ORIGIN + '/ai-hub/?' + params.toString();
|
|
217
|
+
|
|
218
|
+
hubFrame.addEventListener('load', function() {
|
|
219
|
+
setTimeout(function() {
|
|
220
|
+
readFullContext(function(ctx) {
|
|
221
|
+
var msg = { type: 'word-context', payload: ctx };
|
|
222
|
+
if (hubReady) { pushToHub(msg); } else { pendingPush = msg; }
|
|
223
|
+
});
|
|
224
|
+
}, 300);
|
|
225
|
+
addSelectionHandler();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof Office !== 'undefined') {
|
|
230
|
+
Office.onReady(function() { mountHub(); });
|
|
231
|
+
} else {
|
|
232
|
+
window.addEventListener('DOMContentLoaded', mountHub);
|
|
233
|
+
}
|
|
234
|
+
</script>
|
|
235
|
+
</body>
|
|
236
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
|
3
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
4
|
+
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
|
5
|
+
xsi:type="TaskPaneApp">
|
|
6
|
+
<Id>e7a3c812-91fd-4b2e-8c15-d4f6a903b71e</Id>
|
|
7
|
+
<Version>1.0.0.0</Version>
|
|
8
|
+
<ProviderName>FRAIM</ProviderName>
|
|
9
|
+
<DefaultLocale>en-US</DefaultLocale>
|
|
10
|
+
<DisplayName DefaultValue="FRAIM"/>
|
|
11
|
+
<Description DefaultValue="FRAIM AI Hub in PowerPoint - analyze, draft, and improve presentations."/>
|
|
12
|
+
<IconUrl DefaultValue="http://127.0.0.1:43091/powerpoint-taskpane/icon-64.png"/>
|
|
13
|
+
<HighResolutionIconUrl DefaultValue="http://127.0.0.1:43091/powerpoint-taskpane/icon-64.png"/>
|
|
14
|
+
<SupportUrl DefaultValue="http://127.0.0.1:43091"/>
|
|
15
|
+
<AppDomains>
|
|
16
|
+
<AppDomain>https://appsforoffice.microsoft.com</AppDomain>
|
|
17
|
+
</AppDomains>
|
|
18
|
+
<Hosts>
|
|
19
|
+
<Host Name="Presentation"/>
|
|
20
|
+
</Hosts>
|
|
21
|
+
<Requirements>
|
|
22
|
+
<Sets>
|
|
23
|
+
<Set Name="PowerPointApi" MinVersion="1.1"/>
|
|
24
|
+
</Sets>
|
|
25
|
+
</Requirements>
|
|
26
|
+
<DefaultSettings>
|
|
27
|
+
<SourceLocation DefaultValue="http://127.0.0.1:43091/powerpoint-taskpane/"/>
|
|
28
|
+
</DefaultSettings>
|
|
29
|
+
<Permissions>ReadWriteDocument</Permissions>
|
|
30
|
+
</OfficeApp>
|