PageSourceSearch

https://thekebabsshop.co.uk/js/home-agent-tools.js?v=m6wLa6ZVeH5aPIwLbey3cqhrpExclQ_hZnm0DUhkUBI

js thekebabsshop.co.uk collected 2026-10-02 02:09:47 UTC 10,283 bytes, 205 lines download raw bytes

1/**
2 * home-agent-tools.js — WebMCP agent tools for the Razor home page.
3 *
4 * The Angular SPA registers the same four ordering tools on its own document
5 * (AgentToolsService); WebMCP tools are per-document, so this server-rendered
6 * page must register them itself or an AI browsing agent landing here sees
7 * none. The two read tools answer from the AndroWeb API (window.AW_HOME
8 * supplies the tenant API base, as in home-customer.js). The two ordering
9 * tools cannot act here — this page has no basket — so they direct the agent
10 * to the store menu page, where the SPA's full implementations take over.
11 *
12 * Every fetch is memoised and fails soft (resolves null): a failed call makes
13 * the tool answer with a not-available message instead of throwing at the
14 * agent. Decision logic lives in pure helpers on window.AW_AGENT_TOOL_HELPERS
15 * (no DOM access) so a future unit harness can cover it without page
16 * scaffolding.
17 */
18(function () {
19    'use strict';
20
21    var cfg = window.AW_HOME || {};
22
23    // ── pure helpers ─────────────────────────────────────────────────────
24    var helpers = {
25        /** "1 High Street, Testville, AB1 2CD" from the wire Address. */
26        formatAddress: function (address) {
27            var parts = address ? [address.Line1, address.Line2, address.Line3, address.Town, address.Postcode] : [];
28            return parts.filter(function (part) { return part && part.trim().length > 0; }).join(', ');
29        },
30
31        /** The occasion prices are quoted in: the site's first supported occasion, as in the SPA without a basket. */
32        resolveOccasion: function (site) {
33            return (site && site.OccasionsSupported && site.OccasionsSupported[0]) || 'Collection';
34        },
35
36        /** The variant the SPA would quote: the preferred default, else the first. */
37        preferredVariant: function (product) {
38            var variants = (product && product.Variants) || [];
39            return variants.filter(function (variant) { return variant.PreferredDefault; })[0] || variants[0] || null;
40        },
41
42        /** The price for the occasion, falling back to the variant's first price. */
43        priceForVariant: function (variant, occasion) {
44            var prices = (variant && variant.Prices) || [];
45            var match = prices.filter(function (price) { return price.Occasion === occasion; })[0] || prices[0];
46            return match ? match.Amount : undefined;
47        },
48
49        /** {id, name, price} results for products whose name or description contains the query. */
50        searchMenu: function (menu, query, occasion) {
51            var term = (query || '').trim().toLowerCase();
52            if (!term || !menu || !menu.Products) { return []; }
53
54            return menu.Products.filter(function (product) {
55                var name = (product.Name || '').toLowerCase();
56                var description = (product.Description || '').toLowerCase();
57                return name.indexOf(term) !== -1 || description.indexOf(term) !== -1;
58            }).map(function (product) {
59                var variant = helpers.preferredVariant(product);
60                return variant ? { id: variant.Id, name: product.Name, price: helpers.priceForVariant(variant, occasion) } : null;
61            }).filter(function (result) { return result !== null; });
62        },
63
64        /** The site's menu path for its best occasion (delivery first, as the order card does), or null. */
65        menuPathForSite: function (site, siteMenuPaths) {
66            var paths = (siteMenuPaths || {})[site && site.Id];
67            if (!paths) { return null; }
68            var supportsDelivery = ((site && site.OccasionsSupported) || []).indexOf('Delivery') !== -1;
69            return supportsDelivery ? paths.delivery : paths.collection;
70        }
71    };
72    window.AW_AGENT_TOOL_HELPERS = helpers;
73
74    // The API lives at document.modelContext from Chrome 150 (navigator.modelContext before that).
75    var modelContext = document.modelContext || navigator.modelContext;
76    if (!modelContext || !cfg.tenantApiUrl) { return; }
77
78    // ── memoised, fail-soft API reads (sites and menus are anonymous endpoints) ──
79    var memo = {};
80    function getJson(url) {
81        if (!(url in memo)) {
82            memo[url] = fetch(url)
83                .then(function (response) { return response.ok ? response.json() : null; })
84                .catch(function () { return null; });
85        }
86        return memo[url];
87    }
88
89    function getSites() { return getJson(cfg.tenantApiUrl + '/sites'); }
90    function getMenu(siteId) { return getJson(cfg.tenantApiUrl + '/sites/' + encodeURIComponent(siteId) + '/v2/menu'); }
91
92    function menuUrlFor(site) {
93        var path = helpers.menuPathForSite(site, cfg.siteMenuPaths);
94        return path ? window.location.origin + path : null;
95    }
96
97    /** The tenant's only site, or null when there are several (the agent must pick a store on a menu page). */
98    function getSingleSite() {
99        return getSites().then(function (sites) { return sites && sites.length === 1 ? sites[0] : null; });
100    }
101
102    function buildStoreDirectory(sites) {
103        return {
104            stores: sites.map(function (site) {
105                return { name: site.Name, address: helpers.formatAddress(site.Address), phone: site.Phone, menuUrl: menuUrlFor(site) };
106            }),
107            message: 'This brand has several stores. Open a store menu page for store-specific details and ordering.'
108        };
109    }
110
111    var tools = [
112        {
113            name: 'search_menu',
114            description: 'Search the current restaurant menu by keyword and return matching items with their id, name and price.',
115            annotations: { readOnlyHint: true },
116            inputSchema: {
117                type: 'object',
118                properties: {
119                    query: { type: 'string', description: 'Words to match against item names and descriptions.' }
120                },
121                required: ['query']
122            },
123            execute: function (input) {
124                return getSingleSite().then(function (site) {
125                    if (!site) {
126                        return { results: [], total: 0, message: 'Open a store menu page to search that store’s menu.' };
127                    }
128                    return getMenu(site.Id).then(function (menu) {
129                        var results = helpers.searchMenu(menu, input && input.query, helpers.resolveOccasion(site));
130                        return { results: results, total: results.length };
131                    });
132                });
133            }
134        },
135        {
136            name: 'get_store_info',
137            description: 'Get the current store name, address, phone number, supported order occasions and opening hours.',
138            annotations: { readOnlyHint: true },
139            inputSchema: { type: 'object', properties: {} },
140            execute: function () {
141                return getSites().then(function (sites) {
142                    if (!sites || sites.length === 0) {
143                        return { message: 'Store information is not available yet.' };
144                    }
145                    if (sites.length > 1) {
146                        return buildStoreDirectory(sites);
147                    }
148                    var site = sites[0];
149                    return {
150                        name: site.Name,
151                        address: helpers.formatAddress(site.Address),
152                        phone: site.Phone,
153                        occasions: site.OccasionsSupported || [],
154                        openingHours: site.OpeningHours || []
155                    };
156                });
157            }
158        },
159        {
160            name: 'add_to_basket',
161            description: 'Add a menu item to the current basket by its id (as returned by search_menu).',
162            inputSchema: {
163                type: 'object',
164                properties: {
165                    itemId: { type: 'string', description: 'The item id returned by search_menu.' },
166                    quantity: { type: 'integer', minimum: 1, description: 'How many to add. Defaults to 1.' }
167                },
168                required: ['itemId']
169            },
170            execute: function () {
171                return getSingleSite().then(function (site) {
172                    var menuUrl = site ? menuUrlFor(site) : null;
173                    return {
174                        added: false,
175                        message: 'Items are added from the store menu page, not the home page.' + (menuUrl ? ' Open ' + menuUrl + ' and call add_to_basket there.' : ' Open a store menu page and call add_to_basket there.')
176                    };
177                });
178            }
179        },
180        {
181            name: 'start_checkout',
182            description: 'Return the checkout URL for the current basket so the customer can review and pay.',
183            inputSchema: { type: 'object', properties: {} },
184            execute: function () {
185                return getSingleSite().then(function (site) {
186                    var menuUrl = site ? menuUrlFor(site) : null;
187                    return {
188                        started: false,
189                        message: 'There is no basket on the home page. Add items from the store menu page first' + (menuUrl ? ': ' + menuUrl : '.')
190                    };
191                });
192            }
193        }
194    ];
195
196    // Each registration is isolated: registerTool's shape varies across Chrome versions
197    // (it can throw synchronously or return void instead of a promise), and one bad tool
198    // must not stop the remaining tools from registering. Failures stay silent, matching
199    // this page's fail-soft scripts — the tools are an enhancement, never a page break.
200    tools.forEach(function (tool) {
201        try {
202            Promise.resolve(modelContext.registerTool(tool)).catch(function () { /* fail soft */ });
203        } catch (e) { /* fail soft */ }
204    });
205})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.