@tasksai/install 0.1.38 → 0.1.40

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.
Files changed (74) hide show
  1. package/README.md +34 -0
  2. package/bootstrap/Install RealtorTasksAI.command +37 -0
  3. package/bootstrap/Install RealtorTasksAI.ps1 +42 -0
  4. package/package.json +14 -3
  5. package/runtime/document_renderer.py +882 -0
  6. package/runtime/server.py +55 -581
  7. package/runtime/software_use.py +39 -0
  8. package/runtime/workflow-requirements.txt +5 -0
  9. package/runtime/workflows/__init__.py +0 -0
  10. package/runtime/workflows/account_snapshot.py +33 -0
  11. package/runtime/workflows/attachments.py +45 -0
  12. package/runtime/workflows/authority_guides.py +81 -0
  13. package/runtime/workflows/catalog.py +51 -0
  14. package/runtime/workflows/catalog_app.py +174 -0
  15. package/runtime/workflows/catalog_generation.py +186 -0
  16. package/runtime/workflows/catalog_output.py +312 -0
  17. package/runtime/workflows/catalog_suggestions.py +51 -0
  18. package/runtime/workflows/catalog_workspace.py +152 -0
  19. package/runtime/workflows/cli.py +66 -0
  20. package/runtime/workflows/document_answers.py +96 -0
  21. package/runtime/workflows/document_selection.py +50 -0
  22. package/runtime/workflows/documents.py +243 -0
  23. package/runtime/workflows/embedded/catalog.html +83 -0
  24. package/runtime/workflows/embedded/offer-review.html +516 -0
  25. package/runtime/workflows/embedded/preview.html +36 -0
  26. package/runtime/workflows/embedded_actions.py +96 -0
  27. package/runtime/workflows/embedded_demo.py +424 -0
  28. package/runtime/workflows/folders.py +120 -0
  29. package/runtime/workflows/gateway.py +157 -0
  30. package/runtime/workflows/generation_lock.py +26 -0
  31. package/runtime/workflows/launcher.py +61 -0
  32. package/runtime/workflows/licensed_delivery.py +46 -0
  33. package/runtime/workflows/mcp_dev.py +52 -0
  34. package/runtime/workflows/meeting_plan.py +92 -0
  35. package/runtime/workflows/model_client.py +26 -0
  36. package/runtime/workflows/numeric_consistency.py +72 -0
  37. package/runtime/workflows/public_source_fetch.py +66 -0
  38. package/runtime/workflows/realtor/__init__.py +0 -0
  39. package/runtime/workflows/realtor/adapter.py +179 -0
  40. package/runtime/workflows/realtor/seller_offer/ORIGIN.json +16 -0
  41. package/runtime/workflows/realtor/seller_offer/__init__.py +0 -0
  42. package/runtime/workflows/realtor/seller_offer/examples/demo-input.json +414 -0
  43. package/runtime/workflows/realtor/seller_offer/examples/make_demo.py +44 -0
  44. package/runtime/workflows/realtor/seller_offer/references/input-contract.md +65 -0
  45. package/runtime/workflows/realtor/seller_offer/references/source-boundaries.md +16 -0
  46. package/runtime/workflows/realtor/seller_offer/scripts/__init__.py +0 -0
  47. package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.mjs +233 -0
  48. package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.py +163 -0
  49. package/runtime/workflows/realtor/seller_offer/scripts/offer_engine.py +370 -0
  50. package/runtime/workflows/realtor/seller_offer/scripts/presentation.py +52 -0
  51. package/runtime/workflows/realtor/seller_offer/scripts/render_outputs.py +228 -0
  52. package/runtime/workflows/realtor/seller_offer/scripts/run_package.py +95 -0
  53. package/runtime/workflows/realtor/seller_offer/tests/test_engine.py +252 -0
  54. package/runtime/workflows/realtor/seller_offer/tests/test_workbook.mjs +28 -0
  55. package/runtime/workflows/realtor-release-registry.json +705 -0
  56. package/runtime/workflows/released_catalog.py +91 -0
  57. package/runtime/workflows/source_capture.py +82 -0
  58. package/runtime/workflows/store.py +492 -0
  59. package/runtime/workflows/table_calculations.py +132 -0
  60. package/runtime/workflows/template.py +65 -0
  61. package/runtime/workflows/workspace_launch.py +76 -0
  62. package/src/index.js +217 -118
  63. package/src/managed-python.js +63 -0
  64. package/src/operation-lock.js +22 -0
  65. package/src/prepared-update.js +86 -0
  66. package/src/private-workflow.js +116 -0
  67. package/src/python-runtime.js +50 -0
  68. package/src/recover-installation.js +30 -0
  69. package/src/recovery-lock.js +30 -0
  70. package/src/software-hash.js +24 -0
  71. package/src/software-use.js +32 -0
  72. package/src/update-journal.js +24 -0
  73. package/src/update-recovery.js +72 -0
  74. package/src/workspace-runtime.js +45 -0
@@ -0,0 +1,516 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>RealtorTasksAI · Offer comparison</title>
6
+ <style>
7
+ :root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; color: #243731; background: #fff; }
8
+ * { box-sizing: border-box; }
9
+ body { margin: 0; padding: 28px; }
10
+ main { max-width: 760px; margin: auto; }
11
+ .brandbar { display:flex; align-items:center; justify-content:space-between; gap:16px; padding-bottom:22px; margin-bottom:24px; border-bottom:1px solid #e0e9e4; }
12
+ .brand { display:flex; align-items:center; gap:9px; font-weight:750; letter-spacing:-.045em; font-size:20px; }
13
+ .brand svg { width:29px; height:29px; color:#347462; }
14
+ .brand span span { color:#347462; }
15
+ .credits { text-align:right; white-space:nowrap; font-size:12px; color:#66736e; }
16
+ .credits strong { color:#243731; font-size:18px; margin-right:4px; }
17
+ .heading-row { display:flex; align-items:center; justify-content:space-between; gap:12px; }
18
+ .badge { color:#806132; background:#f8f1e4; padding:5px 10px; border-radius:20px; font-size:12px; white-space:nowrap; }
19
+ .saved { font-size:12px; color:#49766a; }
20
+ .section-heading { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:24px 0 12px; }
21
+ .section-heading h2 { margin:0; }
22
+ .count { display:inline-grid; place-items:center; width:23px; height:23px; margin-left:6px; border-radius:50%; font-size:12px; background:#f8f1e4; color:#806132; }
23
+ .eyebrow { color: #49766a; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
24
+ h1 { font-size: 24px; line-height: 1.2; letter-spacing: -.04em; margin: 8px 0; }
25
+ .muted { color: #66736e; font-size: 13px; }
26
+ .offers { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0 12px; }
27
+ .offer { padding: 18px; background: #f2f6f4; border: 1px solid #e0e9e4; border-radius: 12px; }
28
+ .amount { display: block; font-size: clamp(21px, 4vw, 30px); font-weight: 650; letter-spacing: -.04em; margin: 4px 0; }
29
+ h2 { margin: 24px 0 12px; font-size: 17px; }
30
+ .files { display: flex; gap: 10px; flex-wrap: wrap; }
31
+ button { font: inherit; font-weight: 600; border-radius: 9px; padding: 12px 16px; border: 1px solid #244c40; color: #fff; background: #244c40; cursor: pointer; }
32
+ button.secondary { color: #244c40; background: transparent; }
33
+ button:hover { filter: brightness(.92); }
34
+ button:focus-visible { outline: 3px solid #68a48f; outline-offset: 3px; }
35
+ button:disabled { cursor: wait; opacity: .55; }
36
+ ul { list-style: none; padding: 0; margin: 0; }
37
+ li { padding: 14px 16px; border-left: 3px solid #ba884b; background: #fcf8f0; margin: 10px 0; border-radius: 0 8px 8px 0; }
38
+ li strong { display: block; margin-bottom: 4px; font-size: 14px; }
39
+ li p { margin: 0; font-size: 14px; color: #5f5545; }
40
+ footer { border-top: 1px solid #e0e9e4; padding-top: 16px; margin-top: 22px; }
41
+ #status { min-height: 21px; font-size: 13px; margin: 10px 0 0; }
42
+ #status[data-error="true"] { color: #a62c24; }
43
+ .quiet {background:transparent;color:#347462;border-color:#cbdcd3;padding:7px 11px;font-size:13px;}
44
+ .project-nav {display:flex;justify-content:space-between;align-items:center;margin:-8px 0 22px;gap:10px;}
45
+ .attention-action {margin-top:10px;}
46
+ .note {margin-top:10px!important;padding-top:10px;border-top:1px solid #e8decc;}
47
+ form {padding:18px;background:#f2f6f4;border:1px solid #d9e4dd;border-radius:10px;margin:18px 0;}
48
+ form h2 {margin:0 0 12px;}
49
+ label {display:block;font-size:13px;font-weight:600;margin:12px 0 5px;}
50
+ input,textarea {box-sizing:border-box;width:100%;font:inherit;color:inherit;background:#fff;border:1px solid #bfcec5;border-radius:6px;padding:10px;}
51
+ textarea {min-height:76px;resize:vertical;}
52
+ .form-actions {display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;}
53
+ .project-row {border:1px solid #d9e4dd;border-radius:9px;padding:14px;margin:10px 0;background:transparent;}
54
+ .project-row button {display:block;text-align:left;width:100%;border:0;background:transparent;color:inherit;padding:0;}
55
+ .project-row small {display:block;font-weight:400;color:#66736e;margin-top:4px;}
56
+ #project-search {margin:12px 0;}
57
+ [hidden] { display: none !important; }
58
+ @media(max-width: 420px) { body { padding: 16px; } .offers { gap: 8px; } .offer { padding: 12px; } .files { flex-direction: column; } .brand{font-size:17px;gap:5px}.brand svg{width:23px}.credits{font-size:10px}.credits strong{font-size:16px}.heading-row{align-items:start}.badge{font-size:11px} }
59
+ @media(prefers-color-scheme: dark) {
60
+ :root { background: #18211e; color: #ecf3ef; }
61
+ .muted { color: #b1c0b8; } .eyebrow { color: #a4cdbb; }
62
+ .credits,.saved {color:#b1c0b8}.credits strong{color:#ecf3ef}.brand span span,.brand svg{color:#a4cdbb}.brandbar{border-color:#3c4a43}.badge,.count{background:#322c21;color:#e3d2b6}
63
+ .offer { background: #24322b; border-color: #3c4a43; }
64
+ li { background: #322c21; } li p { color: #e3d2b6; }
65
+ button { background: #b6dbc7; color: #162c21; border-color: #b6dbc7; }
66
+ button.secondary { color: #b6dbc7; } footer { border-color: #3c4a43; }
67
+ #status[data-error="true"] { color: #ffb5ac; }
68
+ form{background:#24322b;border-color:#3c4a43}input,textarea{background:#18211e;border-color:#52685a}.quiet{color:#b6dbc7;border-color:#52685a}.project-row small{color:#b1c0b8}.note{border-color:#65563f}
69
+ }
70
+ </style>
71
+ <main>
72
+ <div class="brandbar">
73
+ <div class="brand"><svg viewBox="0 0 32 32" fill="none" aria-hidden="true"><path d="M4 15 16 5l12 10M8 13v14h16V13M13 27v-8h6v8" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"/></svg><span id="brand-name">RealtorTasksAI</span></div>
74
+ <div class="credits" id="credits" hidden title="Illustrative balance for this customer-experience preview"><strong id="credit-count"></strong>credits available</div>
75
+ </div>
76
+ <div class="project-nav"><button class="quiet" id="saved-projects" type="button">Saved projects</button><span id="assigned-reviewer" class="muted"></span></div>
77
+ <section id="project-list" hidden><h1>Saved projects</h1><label for="project-search">Find a project</label><input id="project-search" type="search" placeholder="Search by name"><ul id="projects"></ul></section>
78
+ <div id="project-heading">
79
+ <div class="eyebrow" id="workflow-title">Offer comparison</div>
80
+ <div class="heading-row"><h1 id="title">Opening your saved comparison…</h1><span class="badge" id="review" hidden></span></div>
81
+ </div>
82
+ <div id="content" hidden>
83
+ <div class="section-heading" id="summary-heading"><h2>At a glance</h2><span class="saved">✓ Saved</span></div>
84
+ <div class="offers" id="offers"></div>
85
+ <div class="section-heading"><h2>Your documents</h2><span class="muted">Current version</span></div>
86
+ <div class="files">
87
+ <button id="word" type="button">Download Word briefing</button>
88
+ <button id="excel" class="secondary" type="button">Download Excel comparison</button>
89
+ </div>
90
+ <p id="status" role="status" aria-live="polite"></p>
91
+ <section id="local-folder-controls" hidden>
92
+ <h2>Project folders</h2>
93
+ <button id="choose-source-folder" class="secondary" type="button">Choose source folder</button>
94
+ <button id="scan-source-folder" class="secondary" type="button">Scan source folder</button>
95
+ <p id="source-folder-label" class="muted"></p>
96
+ <button id="choose-results-folder" class="secondary" type="button">Save results to…</button>
97
+ <button id="save-results-folder" type="button">Save current Word / Excel files</button>
98
+ <p id="results-folder-label" class="muted"></p>
99
+ <p class="muted">Scans include PDF, Word and text files in subfolders. Originals stay unchanged. Results are saved in a new version folder under TasksAI Results.</p>
100
+ <p id="folder-status" role="status"></p>
101
+ <h2>Find answers in documents</h2>
102
+ <p class="muted">Select up to 10 documents. Their extracted text will be sent to your signed-in AI assistant to suggest answers. Review suggestions before saving.</p>
103
+ <div id="answer-documents"></div>
104
+ <button id="find-document-answers" type="button">Find suggested answers</button>
105
+ <p id="answer-status" role="status"></p>
106
+ <div id="document-suggestions"></div>
107
+ </section>
108
+ <section><h2>Supporting documents</h2>
109
+ <p class="muted">Upload once and reuse in this project. Fictional files only in this private trial. PDF, Word, or text, up to 2 MB.</p>
110
+ <label for="supporting-file">Choose a document</label><input id="supporting-file" type="file" accept=".pdf,.docx,.txt,.md">
111
+ <button id="upload-document" type="button" disabled>Save document to project</button>
112
+ <p id="upload-status" role="status"></p><ul id="supporting-documents"></ul>
113
+ <section id="source-preview" hidden><h2 id="source-title-preview"></h2><div id="source-parts" style="max-height:350px;overflow:auto;white-space:pre-wrap"></div><button id="close-source" type="button" class="secondary">Close document text</button></section>
114
+ </section>
115
+ <h2>Needs your attention <span class="count" id="attention-count"></span></h2>
116
+ <ul id="attention"></ul>
117
+ <section id="review-section" hidden><h2 id="review-heading">Awaiting review</h2><p id="review-record"></p><ul id="awaiting-review"></ul></section>
118
+ <button id="mark-reviewed" type="button">Mark reviewed</button>
119
+ <button id="edit-workflow" type="button" class="secondary" hidden>Edit meeting details</button>
120
+ <form id="workflow-form" hidden><h2>Meeting details</h2><p class="muted">These details are optional. Leave them blank for a general meeting plan.</p><div id="workflow-fields"></div><button type="submit" id="save-workflow">Save and update Word</button><button type="button" id="cancel-workflow" class="secondary">Cancel</button><p id="workflow-status" role="status"></p></form>
121
+ <form id="attention-form" hidden>
122
+ <h2 id="form-title"></h2>
123
+ <div id="reviewer-fields"><label for="reviewer-name">Reviewer name</label><input id="reviewer-name" maxlength="240" placeholder="Your name or another professional’s name"></div>
124
+ <label id="review-confirmation" hidden><input id="review-confirmed" type="checkbox"> I have reviewed the current documents and saved answers.</label>
125
+ <div id="clarification-fields">
126
+ <p class="muted">Save what you know. Missing information stays flagged. Both documents update once all answers are supplied.</p>
127
+ <label for="financing">Correct financing description</label><input id="financing" maxlength="240" placeholder="For example: Cash purchase">
128
+ <label for="evidence">Supporting financing evidence</label><textarea id="evidence" maxlength="240" placeholder="Describe the supporting evidence and its status"></textarea>
129
+ <label for="explanation">What resolves or explains the discrepancy?</label><textarea id="explanation" maxlength="240" placeholder="Explain the correction for the reviewer"></textarea>
130
+ <label for="source-title">Source of this clarification</label><input id="source-title" maxlength="240" placeholder="Document, email, or conversation being cited">
131
+ <label for="source-date">Source date</label><input id="source-date" type="date">
132
+ </div>
133
+ <div class="form-actions"><button id="save-attention" type="submit">Save and update both files</button><button id="cancel-attention" class="secondary" type="button">Cancel</button></div>
134
+ <p id="form-status" role="status" class="muted"></p>
135
+ </form>
136
+ <footer class="muted">Your project and documents are saved together.</footer>
137
+ </div>
138
+ <p id="loading" role="status">Connecting to your AI workspace…</p>
139
+ </main>
140
+ <script>
141
+ (() => {
142
+ const $ = id => document.getElementById(id);
143
+ let state, nextId = 0, editing, lastSubmission, busy = false, projectItems = [];
144
+ const pending = new Map();
145
+ const notify = (method, params) => window.parent.postMessage({jsonrpc: '2.0', method, params}, '*');
146
+ const request = (method, params) => new Promise((resolve, reject) => {
147
+ const id = ++nextId;
148
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error('The connection took too long. Try again with the same details, or reopen the project.')); }, 60000);
149
+ pending.set(id, {resolve, reject, timer});
150
+ window.parent.postMessage({jsonrpc: '2.0', id, method, params}, '*');
151
+ });
152
+ const renderProjects = () => {
153
+ $('projects').replaceChildren();
154
+ const matches = projectItems.filter(p => p.matter.toLowerCase().includes($('project-search').value.toLowerCase()));
155
+ for (const project of matches) {
156
+ const li = document.createElement('li'); li.className = 'project-row';
157
+ const button = document.createElement('button'); button.type = 'button'; button.textContent = project.matter;
158
+ const detail = document.createElement('small'); detail.textContent = `${project.attention_count} ${project.attention_count === 1 ? 'item' : 'items'} to check · ${project.review_record ? 'Reviewed by ' + project.review_record.reviewer : project.reviewer ? 'Awaiting review · Reviewer: ' + project.reviewer : 'Awaiting review · Reviewer not assigned'}`;
159
+ button.append(detail); button.addEventListener('click', () => loadProject(project.job_id)); li.append(button); $('projects').append(li);
160
+ }
161
+ if (!matches.length) { const li = document.createElement('li'); li.textContent = 'No matching saved projects.'; $('projects').append(li); }
162
+ };
163
+ const render = result => {
164
+ if (localFolders && result?.structuredContent?.job_id && result.structuredContent.job_id !== state?.job_id) {
165
+ const incoming = result.structuredContent.job_id;
166
+ $('source-folder-label').textContent = ''; $('results-folder-label').textContent = ''; $('folder-status').textContent = '';
167
+ fetch(location.pathname.replace(/\/widget$/, '') + '/local-folders', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({job_id:incoming, action:'settings'})})
168
+ .then(r => r.json()).then(settings => { if (state?.job_id === incoming) {
169
+ $('source-folder-label').textContent = settings.input_folder || 'No source folder chosen';
170
+ $('results-folder-label').textContent = settings.output_folder || 'No results folder chosen';
171
+ }}).catch(() => {});
172
+ }
173
+ if (result?.structuredContent?.view === 'projects') {
174
+ projectItems = result.structuredContent.projects;
175
+ const credits = result.structuredContent.credits;
176
+ $('credits').hidden = !Number.isFinite(credits?.available);
177
+ if (!$('credits').hidden) $('credit-count').textContent = new Intl.NumberFormat().format(credits.available);
178
+ $('project-list').hidden = false; $('content').hidden = true; $('project-heading').hidden = true; $('loading').hidden = true;
179
+ $('assigned-reviewer').textContent = ''; $('project-search').value = ''; renderProjects(); return;
180
+ }
181
+ if (!result?.structuredContent?.offers || !result.structuredContent.revision) return;
182
+ if (state?.job_id && state.job_id !== result.structuredContent.job_id) {
183
+ uploadSubmission = undefined; $('supporting-file').value = ''; $('upload-document').disabled = true; $('upload-status').textContent = '';
184
+ }
185
+ state = result.structuredContent;
186
+ $('supporting-documents').replaceChildren(); $('source-preview').hidden = true;
187
+ $('answer-documents').replaceChildren(); $('document-suggestions').replaceChildren(); $('answer-status').textContent = '';
188
+ for (const doc of state.documents || []) {
189
+ const label=document.createElement('label'), input=document.createElement('input'); input.type='checkbox'; input.value=doc.id;
190
+ label.append(input, document.createTextNode(doc.filename)); $('answer-documents').append(label);
191
+ }
192
+ for (const document of state.documents || []) {
193
+ const li = window.document.createElement('li');
194
+ const name = window.document.createElement('strong'); name.textContent = document.filename;
195
+ const saved = window.document.createElement('p'); saved.textContent = 'Saved ' + new Date(document.created_at).toLocaleString();
196
+ const view = window.document.createElement('button'); view.type = 'button'; view.className = 'quiet'; view.textContent = 'View saved text';
197
+ view.addEventListener('click', async () => {
198
+ if (busy) return;
199
+ view.disabled = true;
200
+ try {
201
+ const response = await call('tasksai_read_project_document', {job_id: state.job_id, document_id: document.id});
202
+ const source = response.structuredContent;
203
+ $('source-title-preview').textContent = source.filename;
204
+ $('source-parts').textContent = source.parts.map(part => part.locator + '\n' + part.text).join('\n\n');
205
+ $('source-preview').hidden = false;
206
+ } catch(error) { $('upload-status').textContent = error.message; }
207
+ finally { view.disabled = false; }
208
+ });
209
+ li.append(name, saved, view); $('supporting-documents').append(li);
210
+ }
211
+ if (!(state.documents || []).length) {
212
+ const empty = document.createElement('li'); empty.textContent = 'No supporting documents saved yet.'; $('supporting-documents').append(empty);
213
+ }
214
+ $('summary-heading').hidden = !state.offers.length;
215
+ $('edit-workflow').hidden = !state.presentation?.inputs;
216
+ $('workflow-form').hidden = true;
217
+ if (state.presentation) {
218
+ $('brand-name').textContent = state.presentation.brand.name;
219
+ $('workflow-title').textContent = state.presentation.title;
220
+ document.title = state.presentation.brand.name + ' · ' + state.presentation.title;
221
+ const accent = state.presentation.brand.accent;
222
+ if (/^#[0-9a-f]{6}$/i.test(accent)) $('brand-name').style.color = accent;
223
+ for (const kind of ['word', 'excel']) {
224
+ const output = state.presentation.outputs.find(output => output.kind === kind);
225
+ $(kind).hidden = !output;
226
+ if (output) $(kind).textContent = output.label;
227
+ }
228
+ }
229
+ $('project-list').hidden = true; $('project-heading').hidden = false;
230
+ $('assigned-reviewer').textContent = state.reviewer ? 'Reviewer: ' + state.reviewer : '';
231
+ $('title').textContent = state.matter;
232
+ $('review').textContent = state.review_label || 'Awaiting review';
233
+ $('review').hidden = false;
234
+ $('review').title = state.review_status;
235
+ if (Number.isFinite(state.credits?.available)) {
236
+ $('credit-count').textContent = new Intl.NumberFormat().format(state.credits.available);
237
+ $('credits').hidden = false;
238
+ } else $('credits').hidden = true;
239
+ $('review-heading').textContent = state.review_record ? 'Reviewed' : 'Awaiting review';
240
+ $('review-record').textContent = state.review_record ? 'Reviewed by ' + state.review_record.reviewer + ' · ' + new Date(state.review_record.recorded_at).toLocaleString() : '';
241
+ $('mark-reviewed').hidden = !!state.review_record || state.attention.length > 0 || !state.files_current;
242
+ $('attention-count').textContent = state.attention.length;
243
+ $('offers').replaceChildren();
244
+ for (const offer of state.offers) {
245
+ const box = document.createElement('section'); box.className = 'offer';
246
+ const label = document.createElement('strong'); label.textContent = offer.label;
247
+ const amount = document.createElement('span'); amount.className = 'amount'; amount.textContent = offer.subtotal_text;
248
+ const caption = document.createElement('span'); caption.className = 'muted'; caption.textContent = 'After listed costs'; caption.title = state.scope;
249
+ box.append(label, amount, caption); $('offers').append(box);
250
+ }
251
+ $('attention').replaceChildren();
252
+ $('awaiting-review').replaceChildren();
253
+ $('review-section').hidden = !(state.awaiting_review || []).length && !state.review_record;
254
+ for (const item of [...state.attention, ...(state.awaiting_review || [])]) {
255
+ const li = document.createElement('li'), title = document.createElement('strong'), detail = document.createElement('p');
256
+ title.textContent = state.review_record && (state.awaiting_review || []).includes(item) ? 'Reviewed clarification' : item.title; detail.textContent = state.review_record && (state.awaiting_review || []).includes(item) ? 'Your saved answers are included in the reviewed documents.' : item.detail; li.append(title, detail);
257
+ const target = (state.awaiting_review || []).includes(item) ? 'awaiting-review' : 'attention';
258
+ $(target).append(li);
259
+ if (item.missing_fields?.length) {
260
+ const missing = document.createElement('p'); missing.className = 'note'; missing.textContent = 'Still needed: ' + item.missing_fields.join(', '); li.append(missing);
261
+ }
262
+ if (item.saved_explanation) {
263
+ const note = document.createElement('p'); note.className = 'note'; note.textContent = 'Saved clarification: ' + item.saved_explanation; li.append(note);
264
+ }
265
+ if (item.action) {
266
+ const button = document.createElement('button'); button.className = 'quiet attention-action'; button.type = 'button';
267
+ button.textContent = item.action === 'reviewer' ? 'Assign reviewer' : item.draft_saved ? 'Continue clarification' : item.saved_explanation ? 'Update clarification' : 'Add clarification';
268
+ button.addEventListener('click', () => openForm(item)); li.append(button);
269
+ }
270
+ }
271
+ if (!state.attention.length) {
272
+ const li = document.createElement('li'); li.textContent = 'All requested information has been supplied.'; $('attention').append(li);
273
+ }
274
+ $('content').hidden = false; $('loading').hidden = true;
275
+ status(state.notice || '');
276
+ for (const kind of ['word', 'excel']) $(kind).disabled = !state.files_current;
277
+ if (!state.files_current) status('These files need to be refreshed to include the latest saved concerns.', true);
278
+ };
279
+ const status = (text, error = false) => { $('status').textContent = text; $('status').dataset.error = String(error); };
280
+ let hostSnapshotReceived = false;
281
+ window.addEventListener('message', event => {
282
+ if (event.source !== window.parent || event.data?.jsonrpc !== '2.0') return;
283
+ const message = event.data;
284
+ if (Object.hasOwn(message, 'id') && pending.has(message.id)) {
285
+ const item = pending.get(message.id); clearTimeout(item.timer); pending.delete(message.id);
286
+ message.error ? item.reject(new Error(message.error.message || 'The workspace could not complete this action.')) : item.resolve(message.result);
287
+ } else if (message.method === 'ui/notifications/tool-result') openCurrentSnapshot(message.params);
288
+ else if (message.method === 'ui/notifications/tool-cancelled') {
289
+ $('loading').textContent = 'Opening was cancelled. Ask your AI to reopen the trial.';
290
+ }
291
+ });
292
+ const ready = request('ui/initialize', {appInfo: {name: 'tasksai-offer-review', version: '0.1.0'}, appCapabilities: {}, protocolVersion: '2026-01-26'})
293
+ .then(() => notify('ui/notifications/initialized', {}))
294
+ .catch(error => { $('loading').hidden = false; $('loading').textContent = error.message; throw error; });
295
+ ready.catch(() => {});
296
+ const call = async (name, args) => {
297
+ await ready;
298
+ const result = await request('tools/call', {name, arguments: args});
299
+ if (result.isError) throw new Error(result.content?.[0]?.text || 'This action could not be completed.');
300
+ return result;
301
+ };
302
+ async function openCurrentSnapshot(snapshot) {
303
+ if (hostSnapshotReceived) return;
304
+ const data = snapshot?.structuredContent;
305
+ if (!data || (data.view !== 'projects' && !data.job_id)) return;
306
+ hostSnapshotReceived = true;
307
+ busy = true; $('saved-projects').disabled = true;
308
+ $('loading').hidden = false; $('loading').textContent = 'Loading your current saved projects…';
309
+ try {
310
+ const result = data.view === 'projects'
311
+ ? await call('tasksai_saved_projects', {})
312
+ : await call('tasksai_open_project', {job_id: data.job_id});
313
+ render(result);
314
+ } catch(error) {
315
+ $('loading').hidden = false;
316
+ $('loading').textContent = 'Could not refresh saved projects. Click Saved projects to try again. ' + error.message;
317
+ } finally { busy = false; $('saved-projects').disabled = false; }
318
+ }
319
+ async function loadProject(job_id) {
320
+ if (busy) return;
321
+ $('loading').hidden = false; $('loading').textContent = 'Opening saved project…';
322
+ try { render(await call('tasksai_open_project', {job_id})); $('attention-form').hidden = true; }
323
+ catch(error) { $('loading').textContent = error.message; }
324
+ }
325
+ $('saved-projects').addEventListener('click', async () => {
326
+ if (busy) return;
327
+ try { render(await call('tasksai_saved_projects', {})); }
328
+ catch(error) { $('loading').hidden = false; $('loading').textContent = error.message; }
329
+ });
330
+ $('project-search').addEventListener('input', renderProjects);
331
+ const clarificationIds = ['financing','evidence','explanation','source-title','source-date'];
332
+ function updateSaveLabel() {
333
+ const complete = ['reviewer', 'review'].includes(editing?.action) || clarificationIds.every(id => $(id).value.trim());
334
+ $('save-attention').textContent = editing?.action === 'review' ? 'Confirm review' : complete ? 'Save and update both files' : 'Save progress';
335
+ }
336
+ for (const id of clarificationIds) $(id).addEventListener('input', updateSaveLabel);
337
+ function openForm(item) {
338
+ if (busy) return;
339
+ editing = item; lastSubmission = undefined; $('workflow-form').hidden = true;
340
+ $('attention-form').reset(); $('attention-form').hidden = false; $('form-status').textContent = '';
341
+ const review = item.action === 'review';
342
+ const reviewer = item.action === 'reviewer' || review;
343
+ $('review-confirmation').hidden = !review; $('review-confirmed').required = review;
344
+ $('reviewer-name').value = state.reviewer || '';
345
+ $('form-title').textContent = review ? 'Mark this version reviewed' : reviewer ? 'Assign a reviewer' : "Clarify Offer B's financing";
346
+ $('reviewer-fields').hidden = !reviewer; $('reviewer-name').required = reviewer;
347
+ $('clarification-fields').hidden = reviewer;
348
+ for (const id of clarificationIds) {
349
+ $(id).required = false;
350
+ $(id).value = item.form_values?.[id.replace('-', '_')] || '';
351
+ }
352
+ updateSaveLabel();
353
+ $(reviewer ? 'reviewer-name' : 'financing').focus();
354
+ }
355
+ const localFolders = ['127.0.0.1', 'localhost'].includes(location.hostname);
356
+ $('local-folder-controls').hidden = !localFolders;
357
+ async function folderAction(action) {
358
+ if (busy || !state?.job_id) return;
359
+ busy = true;
360
+ const jobId = state.job_id;
361
+ const buttons = ['choose-source-folder','scan-source-folder','choose-results-folder','save-results-folder'];
362
+ buttons.forEach(id => $(id).disabled = true);
363
+ $('folder-status').textContent = action === 'input' || action === 'output' ? 'Choose a folder in the system dialog…' : 'Working with your local folder…';
364
+ try {
365
+ const prefix = location.pathname.replace(/\/widget$/, '');
366
+ const response = await fetch(prefix + '/local-folders', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({job_id:jobId, action})});
367
+ const result = await response.json();
368
+ if (!response.ok) throw new Error(result.error || 'Folder action failed');
369
+ if (state.job_id !== jobId) return;
370
+ if ('input_folder' in result) $('source-folder-label').textContent = result.input_folder || 'No source folder chosen';
371
+ if ('output_folder' in result) $('results-folder-label').textContent = result.output_folder || 'No results folder chosen';
372
+ if (result.view) render({structuredContent:result.view});
373
+ $('folder-status').textContent = result.saved ? `${result.saved.length} documents saved. ${result.skipped.length} skipped. ` + result.skipped.map(item => item.name + ': ' + item.reason).join('; ') : result.files ? 'Saved ' + result.files.join(', ') + ' to ' + result.folder : 'Folder choice saved for this project.';
374
+ } catch(error) { $('folder-status').textContent = error.message; }
375
+ finally { busy = false; buttons.forEach(id => $(id).disabled = false); }
376
+ }
377
+ $('choose-source-folder').onclick = () => folderAction('input');
378
+ $('scan-source-folder').onclick = () => folderAction('scan');
379
+ $('choose-results-folder').onclick = () => folderAction('output');
380
+ $('save-results-folder').onclick = () => folderAction('save');
381
+ $('find-document-answers').onclick = async () => {
382
+ if (busy) return;
383
+ const document_ids = [...$('answer-documents').querySelectorAll('input:checked')].map(input=>input.value);
384
+ if (!document_ids.length || document_ids.length>10) { $('answer-status').textContent='Select between 1 and 10 documents.'; return; }
385
+ const jobId=state.job_id, revision=state.revision;
386
+ busy=true; $('find-document-answers').disabled=true; $('saved-projects').disabled=true;
387
+ $('answer-status').textContent='Reading the selected documents and checking source references…';
388
+ try {
389
+ const response=await fetch(location.pathname.replace(/\/widget$/, '')+'/local-folders', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'suggest',job_id:jobId,document_ids})});
390
+ const result=await response.json();
391
+ if (!response.ok) throw new Error(result.error || 'Unable to find answers');
392
+ if (state.job_id!==jobId || state.revision!==revision) throw new Error('Project changed; find suggestions again.');
393
+ $('document-suggestions').replaceChildren();
394
+ for (const candidate of result.candidates) {
395
+ const row=document.createElement('section'), title=document.createElement('strong'), value=document.createElement('p'), citation=document.createElement('p'), quote=document.createElement('blockquote'), use=document.createElement('button');
396
+ title.textContent=result.fields[candidate.field]+(candidate.conflict?' — conflicting answers; choose one':'');
397
+ value.textContent=candidate.value; citation.textContent=candidate.filename+' · '+candidate.locator; quote.textContent=candidate.quote;
398
+ use.type='button'; use.textContent='Use this answer';
399
+ use.onclick=()=>{
400
+ if (busy || state.job_id!==jobId || state.revision!==revision) return;
401
+ const source=candidate.filename+' · '+candidate.locator;
402
+ if (state.presentation.inputs) {
403
+ if ($('workflow-form').hidden) $('edit-workflow').click();
404
+ const input=$('workflow-fields').querySelector('[name="'+candidate.field+'"]');
405
+ input.value=candidate.value;
406
+ const notes=$('workflow-fields').querySelector('[name="notes"]');
407
+ const evidence='Source for '+result.fields[candidate.field]+': '+source+' — '+candidate.quote;
408
+ if (notes && !notes.value.includes(evidence)) notes.value=(notes.value+'\n'+evidence).trim();
409
+ $('workflow-status').textContent='Suggested answer added. Check the details, then save to create a new Word version.';
410
+ } else {
411
+ const action=candidate.field==='reviewer'?'reviewer':'clarification';
412
+ const item=[...state.attention,...(state.awaiting_review||[])].find(item=>item.action===action);
413
+ if (!item) return;
414
+ if ($('attention-form').hidden || editing?.action!==action) openForm(item);
415
+ $(candidate.field==='reviewer'?'reviewer-name':candidate.field).value=candidate.value;
416
+ if (action==='clarification') $('source-title').value=source.slice(0,240);
417
+ $('form-status').textContent='Suggested answer added. Check the details and any remaining fields before saving.';
418
+ }
419
+ use.textContent='Added to form';
420
+ };
421
+ row.append(title,value,citation,quote,use); $('document-suggestions').append(row);
422
+ }
423
+ $('answer-status').textContent=result.candidates.length+' suggestions found. '+(result.missing.length?'Still needs information: '+result.missing.map(key=>result.fields[key]).join(', ')+'.':'')+' Nothing has been applied to your documents yet.';
424
+ } catch(error) { $('answer-status').textContent=error.message; }
425
+ finally {busy=false; $('find-document-answers').disabled=false; $('saved-projects').disabled=false;}
426
+ };
427
+ let uploadSubmission;
428
+ $('close-source').addEventListener('click', () => { $('source-preview').hidden = true; });
429
+ $('supporting-file').addEventListener('change', () => {
430
+ uploadSubmission = undefined; $('upload-status').textContent = '';
431
+ $('upload-document').disabled = !$('supporting-file').files.length;
432
+ });
433
+ $('upload-document').addEventListener('click', async () => {
434
+ if (busy) return;
435
+ const file = $('supporting-file').files[0]; if (!file) return;
436
+ if (!file.size || file.size > 2000000) { $('upload-status').textContent = 'Choose a nonempty file up to 2 MB.'; return; }
437
+ busy = true; $('upload-document').disabled = true; $('saved-projects').disabled = true;
438
+ $('upload-status').textContent = 'Saving your document…';
439
+ try {
440
+ if (!uploadSubmission) {
441
+ const encoded = await new Promise((resolve, reject) => {
442
+ const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = () => reject(new Error('Choose the file again.')); reader.readAsDataURL(file);
443
+ });
444
+ uploadSubmission = {job_id: state.job_id, filename: file.name, encoded, request_id: 'upload-' + crypto.randomUUID()};
445
+ }
446
+ const result = await call('tasksai_upload_project_document', uploadSubmission);
447
+ const editingWorkflow = !$('workflow-form').hidden;
448
+ render(result); $('workflow-form').hidden = !editingWorkflow; $('supporting-file').value = ''; uploadSubmission = undefined;
449
+ $('upload-status').textContent = 'Saved. You can reuse this document when you reopen the project.';
450
+ } catch(error) { $('upload-status').textContent = error.message; }
451
+ finally { busy = false; $('saved-projects').disabled = false; $('upload-document').disabled = !$('supporting-file').files.length; }
452
+ });
453
+ let workflowSubmission;
454
+ $('edit-workflow').addEventListener('click', () => {
455
+ if (busy) return;
456
+ $('attention-form').hidden = true;
457
+ $('workflow-fields').replaceChildren(); $('workflow-status').textContent = '';
458
+ workflowSubmission = undefined;
459
+ for (const field of state.presentation.inputs) {
460
+ const label = document.createElement('label'); label.textContent = field.label;
461
+ const input = document.createElement('textarea'); input.name = field.key; input.value = state.answers[field.key] || '';
462
+ input.maxLength = 2000; input.required = field.required; label.append(input); $('workflow-fields').append(label);
463
+ }
464
+ $('workflow-form').hidden = false;
465
+ });
466
+ $('cancel-workflow').addEventListener('click', () => { if (!busy) $('workflow-form').hidden = true; });
467
+ $('workflow-form').addEventListener('submit', async event => {
468
+ event.preventDefault(); if (busy) return;
469
+ const args = {job_id: state.job_id, revision: state.revision, answers: Object.fromEntries(new FormData($('workflow-form')))};
470
+ const fingerprint = JSON.stringify(args);
471
+ if (workflowSubmission?.fingerprint !== fingerprint) workflowSubmission = {fingerprint, id: 'card-' + crypto.randomUUID()};
472
+ args.request_id = workflowSubmission.id;
473
+ busy = true; $('save-workflow').disabled = true; $('saved-projects').disabled = true;
474
+ $('workflow-status').textContent = 'Updating your Word document…';
475
+ try { render(await call('tasksai_save_workflow_inputs', args)); workflowSubmission = undefined; }
476
+ catch(error) { $('workflow-status').textContent = error.message; }
477
+ finally { busy = false; $('save-workflow').disabled = false; $('saved-projects').disabled = false; }
478
+ });
479
+ $('mark-reviewed').addEventListener('click', () => openForm({action: 'review'}));
480
+ $('cancel-attention').addEventListener('click', () => { if (!busy) $('attention-form').hidden = true; });
481
+ $('attention-form').addEventListener('submit', async event => {
482
+ event.preventDefault(); if (busy) return;
483
+ const args = {job_id: state.job_id, revision: state.revision, action: editing.action};
484
+ if (['reviewer', 'review'].includes(editing.action)) args.reviewer = $('reviewer-name').value.trim();
485
+ else Object.assign(args, {concern_id: editing.concern_id, expected_event_id: editing.expected_event_id,
486
+ financing: $('financing').value.trim(), evidence: $('evidence').value.trim(), explanation: $('explanation').value.trim(), source_title: $('source-title').value.trim(), source_date: $('source-date').value});
487
+ if (editing.action === 'review') args.review_confirmed = $('review-confirmed').checked;
488
+ const fingerprint = JSON.stringify(args);
489
+ if (lastSubmission?.fingerprint !== fingerprint) lastSubmission = {fingerprint, id: 'card-' + crypto.randomUUID()};
490
+ args.request_id = lastSubmission.id;
491
+ busy = true; $('save-attention').disabled = true; $('cancel-attention').disabled = true; $('saved-projects').disabled = true;
492
+ $('form-status').textContent = editing.action === 'review' ? 'Recording your review…' : $('save-attention').textContent === 'Save progress' ? 'Saving your progress…' : 'Saving your update and preparing both documents…';
493
+ try {
494
+ const result = await call('tasksai_save_attention', args); render(result); $('attention-form').hidden = true; lastSubmission = undefined;
495
+ } catch(error) { $('form-status').textContent = error.message; }
496
+ finally { busy = false; $('save-attention').disabled = false; $('cancel-attention').disabled = false; $('saved-projects').disabled = false; }
497
+ });
498
+ for (const kind of ['word', 'excel']) $(kind).addEventListener('click', async () => {
499
+ const button = $(kind); button.disabled = true;
500
+ status('Preparing your download…');
501
+ try {
502
+ await ready;
503
+ const result = await request('tools/call', {name: 'tasksai_trial_document', arguments: {kind, revision: state.revision, job_id: state.job_id}});
504
+ if (result.isError) throw new Error(result.content?.[0]?.text || 'The document is unavailable. Reopen the trial.');
505
+ const url = new URL(result._meta?.downloadUrl);
506
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname))) throw new Error('The download address is invalid.');
507
+ const opened = await request('ui/open-link', {url: url.href});
508
+ if (opened?.isError) throw new Error('The workspace could not open the download. Please try again.');
509
+ status('Download requested. Check your browser’s downloads.');
510
+ } catch(error) { status(error.message, true); }
511
+ finally { button.disabled = !state?.files_current; }
512
+ });
513
+ if (typeof ResizeObserver !== 'undefined') new ResizeObserver(() => notify('ui/notifications/size-changed', {height: Math.ceil(document.body.getBoundingClientRect().height)})).observe(document.body);
514
+ })();
515
+ </script>
516
+ </html>
@@ -0,0 +1,36 @@
1
+ <!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
2
+ <title>RealtorTasksAI · Customer experience preview</title>
3
+ <style>
4
+ body{margin:0;background:#e9eeeb;color:#263c31;font:15px/1.5 system-ui,sans-serif;padding:32px 16px}
5
+ header{max-width:760px;margin:0 auto 20px} header p{margin:4px 0;font-size:13px;color:#56665d}
6
+ iframe{display:block;width:100%;max-width:760px;min-height:700px;margin:auto;border:1px solid #d9e2dc;border-radius:16px;background:#fff}
7
+ </style>
8
+ <header><strong>Customer experience preview</strong><p>The card below is the proposed customer view. The 120-credit balance is an example.</p></header>
9
+ <iframe title="Saved offer comparison" id="card"></iframe>
10
+ <script>
11
+ const prefix = location.pathname.replace(/\/preview$/, '');
12
+ const card = document.getElementById('card');
13
+ const respond = (id, result) => card.contentWindow.postMessage({jsonrpc:'2.0',id,result},location.origin);
14
+ window.addEventListener('message',async event=>{
15
+ if(event.source !== card.contentWindow || event.origin !== location.origin || event.data?.jsonrpc !== '2.0')return;
16
+ const {id,method,params} = event.data;
17
+ try {
18
+ if(method==='ui/initialize') respond(id,{protocolVersion:'2026-01-26',hostInfo:{name:'TasksAI local test harness',version:'0.1.0'},hostCapabilities:{openLinks:{},serverTools:{}}});
19
+ if(method==='ui/notifications/initialized') {
20
+ const result=await fetch(prefix+'/preview-data').then(r=>r.json());
21
+ card.contentWindow.postMessage({jsonrpc:'2.0',method:'ui/notifications/tool-result',params:result},location.origin);
22
+ }
23
+ if(method==='tools/call') {
24
+ const result= params.name === 'tasksai_trial_document'
25
+ ? await fetch(prefix+'/preview-document/'+encodeURIComponent(params.arguments.kind)+'?revision='+encodeURIComponent(params.arguments.revision)+(params.arguments.job_id?'&job_id='+encodeURIComponent(params.arguments.job_id):'')).then(r=>r.json())
26
+ : await fetch(prefix+'/preview-tool', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(params)}).then(r=>r.json());
27
+ respond(id,result);
28
+ }
29
+ if(method==='ui/open-link') {
30
+ const link=document.createElement('a');link.href=params.url;link.download='';document.body.append(link);link.click();link.remove();respond(id,{});
31
+ }
32
+ if(method==='ui/notifications/size-changed')card.style.height=Math.max(300,Math.min(2000,params.height+4))+'px';
33
+ }catch(error){card.contentWindow.postMessage({jsonrpc:'2.0',id,error:{code:-32000,message:error.message}},location.origin)}
34
+ });
35
+ card.src=prefix+'/widget';
36
+ </script></html>