Skills › Scientific Agent Skills › fictiv › quote_state.js
quote_state.js
175 lines · skills/fictiv/scripts/quote_state.js at 5f2bc03
1// Fictiv quote-page state extractor.
2//
3// Paste the body of this file into the browser's javascript tool while on
4// https://app.fictiv.com/pages/quotes/<quoteId> (quote page)
5// https://app.fictiv.com/pages/orders/quote/<quoteId> (checkout page)
6// It returns a terse line-per-item digest of what the page shows (QUOTE, STAGE,
7// TIERS, one P<n> line per part, SUMMARY, BUTTONS, CHECKOUT, DIALOG) so you can
8// reason about the quote without screenshots. The full structured object
9// (incl. partIds) is left on window.__fictivState. Read-only: never clicks.
10//
11// The app is built on Ant Design, so selectors lean on stable `ant-*` classes and
12// visible text rather than hashed CSS-module names (e.g. `tableColumns--sfkbO`),
13// which change between Fictiv releases.
15(() => {
16 const txt = (el) => (el ? el.innerText.replace(/\u00a0/g, ' ').trim() : '');
17 const lines = (el) => txt(el).split('\n').map((s) => s.trim()).filter(Boolean);
18 const body = document.body.innerText;
19 const path = location.pathname;
21 const out = {
22 url: location.origin + path,
23 page: /\/pages\/orders\/quote\//.test(path)
24 ? 'checkout'
25 : /\/part\/[^/]+\/(configuration|threads|feedback)/.test(path)
26 ? 'part-modal'
27 : /\/pages\/quotes\/[0-9a-f-]{36}/.test(path)
28 ? 'quote'
29 : 'other',
30 quoteId: (path.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) || [null])[0],
31 quoteName: null,
32 banner: null,
33 useClassification: null,
34 leadTimeTiers: [],
35 usaOnly: null,
36 parts: [],
37 summary: {},
38 buttons: {},
39 openDialogs: [],
40 analyzing: /Analyzing (parts|geometry)|Configuring part|Determining available lead times/.test(body),
41 };
43 // Quote title sits right after "Save and back".
44 const m = body.match(/Save and back\n(.+)\nLast modified/);
45 if (m) out.quoteName = m[1].trim();
47 // Banner above the parts table tells you which stage the quote is in.
48 for (const b of [
49 'Required: Are these parts for prototype or commercial use?',
50 'Configure parts to receive lead times',
51 'Determining available lead times',
52 'Select regional preference',
53 ]) if (body.includes(b)) { out.banner = b; break; }
55 // Prototype / Commercial: shown as two cards before selection, as a small
56 // dropdown (a bare "Prototype"/"Commercial" line) in the banner afterwards.
57 if (out.banner && out.banner.startsWith('Required')) out.useClassification = 'NOT SET';
58 else {
59 const top = body.slice(0, body.indexOf('Select files or drag') > 0 ? body.indexOf('Select files or drag') : 3000);
60 const cm = top.match(/\n(Prototype|Commercial)\n/);
61 out.useClassification = cm ? cm[1] : null;
62 }
64 // Lead-time tiers are radio inputs whose value names the tier.
65 document.querySelectorAll('input[type=radio]').forEach((r) => {
66 if (!/^(domestic|overseas)/.test(r.value)) return;
67 const l = lines(r.closest('label') || r.parentElement);
68 out.leadTimeTiers.push({
69 key: r.value,
70 region: r.value.startsWith('domestic') ? 'North America' : 'Overseas',
71 label: l.find((s) => /production days/.test(s)) || null,
72 price: l.find((s) => /^\$/.test(s)) || null,
73 tag: l.find((s) => /^(Fastest|Standard|Cost-effective)$/.test(s)) || null,
74 selected: r.checked,
75 });
76 });
77 const usa = [...document.querySelectorAll('label')].find((l) => txt(l) === 'USA only');
78 if (usa) { const cb = usa.querySelector('input[type=checkbox]'); out.usaOnly = cb ? cb.checked : null; }
80 // Parts table rows.
81 document.querySelectorAll('tr.ant-table-row').forEach((tr) => {
82 const c = [...tr.children];
83 if (c.length < 6) return;
84 const name = lines(c[1]);
85 const cfg = lines(c[2]).filter((s) => s !== 'Edit configuration' && s !== 'Configure');
86 const link = c[1].querySelector('a[href*="/part/"]');
87 const partId = link ? (link.getAttribute('href').match(/part\/([0-9a-f-]{36})/) || [])[1] : null;
88 out.parts.push({
89 file: name.find((s) => /\.[a-z0-9_]+$/i.test(s)) || name.join(' '),
90 revision: name.find((s) => /^Rev /.test(s)) || null,
91 dfmIssueCount: Number(name.find((s) => /^\d+$/.test(s))) || 0,
92 hasFeedbackLink: name.includes('View feedback'),
93 partId,
94 configured: !lines(c[2]).includes('Configure'),
95 process: cfg[0] || null,
96 material: cfg[1] || null,
97 finishAndOptions: cfg.slice(2),
98 productionDetails: lines(c[3]).join(' ') || null,
99 quantity: (c[4].querySelector('input') || {}).value || null,
100 quantityNote: lines(c[4]).join(' ') || null,
101 price: lines(c[5]),
102 needsManualQuote: /Please request a quote/.test(txt(c[5])),
103 selected: !!(c[0].querySelector('input[type=checkbox]') || {}).checked,
104 });
105 });
107 // Summary card (right column). Grab label/value pairs between "Summary" and the buttons.
108 const sIdx = body.indexOf('\nSummary\n');
109 if (sIdx >= 0) {
110 const seg = body.slice(sIdx + 9, sIdx + 1500).split('\n').map((s) => s.trim()).filter(Boolean);
111 const keys = ['Part production', 'Shipping', 'Tax', 'Order total', 'Ship by', 'Est. Delivery', 'Shipping address'];
112 for (let i = 0; i < seg.length; i++) {
113 const k = keys.find((kk) => seg[i].startsWith(kk));
114 if (k && out.summary[seg[i]] === undefined) out.summary[seg[i]] = seg[i + 1] || null;
115 if (/Your dedicated account manager/.test(seg[i])) break;
116 }
117 const note = seg.find((s) => /Please configure|require a human to quote|Some of your parts/.test(s));
118 if (note) out.summary.note = note;
119 }
121 // Key buttons and whether they're usable.
122 for (const label of ['Request quote', 'Begin checkout', 'Place order', 'Download quote', 'Forward to purchaser',
123 'Bulk configure parts', 'Configure via drawing']) {
124 const b = [...document.querySelectorAll('button')].find((x) => txt(x) === label);
125 if (b) out.buttons[label] = b.disabled || b.getAttribute('aria-disabled') === 'true' ? 'disabled' : 'enabled';
126 }
128 // Checkout-page specifics.
129 if (out.page === 'checkout') {
130 const cut = body.match(/Checkout within .+? cutoff on .+?\./);
131 out.checkout = {
132 cutoffNotice: cut ? cut[0] : null,
133 shippingOptions: /No shipping options available/.test(body) ? 'none (add an address first)' : 'available',
134 payTab: (() => {
135 const t = [...document.querySelectorAll('[role=tab], button, div')].find((x) =>
136 /^(Pay with credit card|Pay with PO)$/.test(txt(x)) && /active|selected|checked/i.test(x.className));
137 return t ? txt(t) : null;
138 })(),
139 savedCards: /Your saved cards\nNo Data/.test(body) ? 0 : 'see page',
140 savedAddresses: /Ship to\nAdd new address\nNo Data/.test(body) ? 0 : 'see page',
141 };
142 }
144 // Any open modal/dialog (e.g. confirmation prompts) — first 400 chars of each.
145 const seen = new Set();
146 document.querySelectorAll('.ant-modal-content, .ant-popover-inner, .ant-drawer-content').forEach((d) => {
147 const t = txt(d);
148 if (t && !seen.has(t) && d.offsetParent !== null) { seen.add(t); out.openDialogs.push(t.slice(0, 400)); }
149 });
151 // Browser JS tools truncate results at roughly 1 KB, so return a terse
152 // line-per-item digest and stash the full object on window for slicing:
153 // window.__fictivState -> full object (JSON.stringify it in pieces)
154 // window.__fictivStateText.slice(1000, 2000) -> next chunk of the digest
155 window.__fictivState = out;
156 const L = [];
157 L.push(`QUOTE ${out.quoteName || '?'} id=${out.quoteId || '?'} page=${out.page} analyzing=${out.analyzing}`);
158 L.push(`STAGE ${out.banner || '-'} | use=${out.useClassification || '-'}${out.usaOnly ? ' | USA-only' : ''}`);
159 if (out.leadTimeTiers.length) {
160 L.push('TIERS ' + out.leadTimeTiers.map((t) =>
161 `${t.selected ? '*' : ''}${t.key}:${(t.label || '').replace(/ production days?/, 'd')}:${t.price}`).join(' | '));
162 }
163 out.parts.forEach((p, i) => {
164 L.push(`P${i + 1} ${p.file} ${p.revision || ''} | ${p.configured ? [p.process, p.material, ...p.finishAndOptions].join(' / ') : 'NOT CONFIGURED'}`
165 + ` | qty ${p.quantity || '-'} | ${p.needsManualQuote ? 'MANUAL QUOTE' : p.price.join(' ')}`
166 + `${p.dfmIssueCount ? ` | DFM ${p.dfmIssueCount}` : ''}${p.productionDetails ? ` | ${p.productionDetails}` : ''}`);
167 });
168 const s = Object.entries(out.summary).map(([k, v]) => (k === 'note' ? `NOTE: ${v.slice(0, 90)}` : `${k}=${v}`));
169 if (s.length) L.push('SUMMARY ' + s.join(' | '));
170 L.push('BUTTONS ' + Object.entries(out.buttons).map(([k, v]) => `${k}:${v}`).join(' | '));
171 if (out.checkout) L.push('CHECKOUT ' + Object.entries(out.checkout).map(([k, v]) => `${k}=${v}`).join(' | '));
172 out.openDialogs.forEach((d) => L.push('DIALOG ' + d.replace(/\n/g, ' / ').slice(0, 200)));
173 window.__fictivStateText = L.join('\n');
174 return window.__fictivStateText;
175})();