bunnyquery 1.9.0 → 1.9.4

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/bunnyquery.js CHANGED
@@ -165,7 +165,22 @@
165
165
  // documents
166
166
  "pdf",
167
167
  "docx",
168
+ "docm",
168
169
  "pptx",
170
+ "pptm",
171
+ "doc",
172
+ "ppt",
173
+ // Korean word processor (OLE/CFB and OOXML-style variants)
174
+ "hwp",
175
+ "hwpx",
176
+ // opendocument text/presentation (ods is a grid, listed above)
177
+ "odt",
178
+ "odp",
179
+ // other long-form documents
180
+ "epub",
181
+ "rtf",
182
+ "html",
183
+ "htm",
169
184
  // plain text / data / markup
170
185
  "txt",
171
186
  "md",
@@ -277,11 +292,13 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
277
292
  // src/engine/prompts/chat_system_prompt.ts
278
293
  function buildChatSystemPrompt(params) {
279
294
  const { projectId, serviceName, serviceDescription, greeting, canUpload} = params;
295
+ const g = params.indexAccessGroup;
296
+ const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
280
297
  let systemPrompt = `
281
298
  You are a dedicated assistant for the project ID: "${projectId}".
282
299
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
283
300
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
284
- Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized" (where the indexer writes; pass access_group explicitly, including 0, to search another group), while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
301
+ Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized", but THIS project indexes at access_group ${indexGroupLiteral}, so pass access_group ${indexGroupLiteral} EXPLICITLY on every index or tag query here - the auto-fill would search a group this project's data is not in and come back empty. Files uploaded before the project's setting changed may sit at another group, so when a scoped query comes back empty, retry it across the other groups (0, 1, "private") before concluding there is nothing, while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
285
302
  Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
286
303
  Embedded values: a search term is often stored inside a larger string. A merchant "BAKSA" appears as "DNH*BAKSA#4070277042", and a card as "5860****5173". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
287
304
  File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
@@ -330,6 +347,7 @@ Project description: """${serviceDescription}"""`;
330
347
  // src/engine/prompts/indexing_system_prompt.ts
331
348
  function buildIndexingSystemPrompt(params) {
332
349
  const { projectId, serviceName, serviceDescription } = params;
350
+ const accessGroup = params.accessGroup === "public" || params.accessGroup === "private" ? params.accessGroup : "authorized";
333
351
  let systemPrompt = `You are a background indexing agent for project ${projectId}.
334
352
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
335
353
  - Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
@@ -338,7 +356,8 @@ Project description: """${serviceDescription}"""`;
338
356
  - VISION: when the message (a readFileContent window, an embedded PDF page, or an inline attachment) includes IMAGES - scanned/rendered PDF pages, or photos embedded in a spreadsheet next to a row/block - LOOK at them and capture what they show as record data (the reading/values in a scanned table, the part/defect/condition visible in a photo). The image IS part of the data; correlate each photo with its labelled block ("PHOTO A3" markers tie a photo to that grid row).
339
357
  - TRANSCRIBE, DO NOT DESCRIBE. When an image contains ANY text - a label, tag, stamp, form field, serial/part number, handwriting - your FIRST job is to read the characters out and store them VERBATIM, not to describe the scene. A record saying "a red inspection tag with handwritten markings" is worthless: it is unsearchable and every such photo produces the same sentence. Put the characters you can actually read into these EXACT fields, not variations of them: "printed_text" (the pre-printed wording), "handwritten_text" (what a person wrote by hand), and, when you can resolve one, "part_no", "tag_id" and "date". Same reason as the fixed table names: a field called photo_text in one pass and visible_text_notes in the next cannot be queried together. Read PARTIAL values rather than skipping: "500.7402.52__" beats nothing. Only when a character is genuinely unreadable, leave that field null or mark the unreadable span - do NOT invent it, and do NOT replace the whole transcription with a description of what the object looks like. A scene description is a nice extra AFTER the text, never instead of it.
340
358
  - IMAGE FILES uploaded as the file itself: if ANY readable character appears ANYWHERE in the image (a label, a stamp, a sign in the background) it counts as an image WITH text - transcribe it per the rule above, and also capture the layout (what appears where) and every entity named. Only a truly text-free image gets description first: a one-line caption, then the objects present with their attributes (type, color, count, condition, position). Either way, save what you extract onto the file's "src::" record with updateRecords, TAG every entity and identifier visible, and INDEX the one number the image offers (a measured value, an amount, a count).
341
- - Whatever the file type, this file's identity is "src::" + its storage path (the "storage path" metadata line) - never the inline content or a temporary URL. That record ALREADY EXISTS: the upload pipeline creates it in table "file_summaries" (access group "authorized") before indexing starts, so posting it again is rejected as a duplicate unique_id. Reference it from every record you write, and add what you learn to it with updateRecords. If that update unexpectedly reports the record does not exist, post it yourself ONCE with that exact "src::" unique_id (table "file_summaries", access group "authorized") and carry on; this is the ONE exception to the do-NOT-post-the-file-record rules elsewhere in these instructions, because the source identity must never be dropped just because an update failed.
359
+ - Whatever the file type, this file's identity is "src::" + its storage path (the "storage path" metadata line) - never the inline content or a temporary URL. That record ALREADY EXISTS: the upload pipeline creates it in table "file_summaries" (access group "${accessGroup}") before indexing starts, so posting it again is rejected as a duplicate unique_id. Reference it from every record you write, and add what you learn to it with updateRecords. If that update unexpectedly reports the record does not exist, post it yourself ONCE with that exact "src::" unique_id (table "file_summaries", access group "${accessGroup}") and carry on; this is the ONE exception to the do-NOT-post-the-file-record rules elsewhere in these instructions, because the source identity must never be dropped just because an update failed.
360
+ - ACCESS GROUP (hard rule): every record you write for this file - the file record, per-row records, chapters, summaries, intermediates - MUST be posted with access group "${accessGroup}". Pass it explicitly on every postRecords call; do not leave it out and do not vary it between passes of the same file. An access group is part of a record's table key, so records saved under a different group than the file are in a different table and will not come back with the rest of it: a "public" file whose rows were saved as "authorized" is one an anonymous visitor can see the name of and none of the contents of, and a re-index cannot find the strays to clean them up. The one exception is the EXTRACTED MEDIA records in "__MEDIA__", which the pipeline creates for you - leave their group alone and only enrich them.
342
361
  - REACHABILITY (hard rule): every record you write while indexing this file MUST be reachable from the file's "src::<storage path>" record by following reference - either reference that record directly, or reference something that already reaches it. A record with no reference, or one pointing outside this file's chain, is an ORPHAN: deleting or re-indexing the file removes the reachable records and leaves the orphan behind forever, where it keeps turning up in later answers as stale data. If you create an intermediate record that OTHER records reference (a page record that rows hang off, a sheet or section record), set source.can_remove_referencing_records to true on it; the delete cascade passes a delete through a record only when that record carries the flag OR a unique_id starting "src::" (the file record cascades because its unique_id starts with "src::"; the intermediates you create carry no "src::" id, so they need the flag), and it cascades ONE LEVEL AT A TIME, so EVERY intermediate record in a chain needs its own marker - an unmarked link stops the cascade there and everything below it survives as orphans. When in doubt, reference the file record directly and keep the chain flat.
343
362
  - TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a table named EXACTLY "spreadsheet_rows". Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, window by window, until it ends. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. The file-level "src::" record ALREADY EXISTS - the upload pipeline creates it before indexing starts - so do NOT create it. Link EVERY per-row record to it via reference (set each row record's reference to exactly "src::" + the storage path, with NO sheet/window/summary suffix added; the row records themselves do NOT carry a src:: unique_id). Enrich that same record with sheet name(s), column headers and total row count via updateRecords rather than posting another one. The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed. INDEX each row record on the row's most useful NUMERIC column (named by its header) so rows sort and range-query; when the row has no numeric column, index the grid row number instead. TAG each row record with the sheet name, the file name, and the row's categorical values (a status, a category, a type) - tags are how rows are filtered without scanning the table.
344
363
  - ONE RECORD PER GRID ROW, ALWAYS. "Row" means the numbered row of the sheet (R37 is one record), never a visual block, item, section or left/right pair. Sheets that repeat the same columns side by side (an A/B block beside a C/D block, "paired" or "mirrored" layouts) still get ONE record per grid row, holding BOTH sides - suffix the keys to keep them apart (PART_NO_A / PART_NO_B). Collapsing a 16-row window into 2 or 3 "block" records is the single most damaging mistake here: it silently loses most of the cells and makes every later total wrong, because some windows were counted per row and others per block. If a window shows rows R37 to R52, you save records for R37..R52 and the count you report is the number of grid rows you actually wrote.
@@ -359,6 +378,10 @@ Project description: """${serviceDescription}"""`;
359
378
  }
360
379
 
361
380
  // src/engine/prompts/indexing_user_message.ts
381
+ function indexingAccessGroup(attachment) {
382
+ const g = attachment && attachment.accessGroup;
383
+ return g === "public" || g === "private" ? g : "authorized";
384
+ }
362
385
  function buildIndexingUserMessage(attachment, options) {
363
386
  const head = `A new file has just been uploaded. Index it now.
364
387
 
@@ -367,7 +390,11 @@ File metadata:
367
390
  - storage path: ${attachment.storagePath}
368
391
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
369
392
  ` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
370
- ` : "");
393
+ ` : "") + // Stated in the metadata block as well as the system prompt because this is
394
+ // the per-FILE value: one project can hold public and private files at once,
395
+ // and the system prompt is what is constant across the run.
396
+ `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
397
+ `;
371
398
  if (options?.inlineContent) {
372
399
  return head + `
373
400
  The file's content was parsed by the client and is provided inline below. Read it directly - do NOT fetch any URL for this file. Set every record's reference to exactly "src::" + the storage path above (not this content). That file record already exists, so enrich it with updateRecords rather than posting it.
@@ -415,7 +442,8 @@ Records for the earlier pages are ALREADY saved (they reference "${src}"). The N
415
442
  - name: ${attachment.name}
416
443
  - storage path: ${attachment.storagePath}
417
444
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
418
- ` : "");
445
+ ` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
446
+ `;
419
447
  }
420
448
  function buildRenderDatafy(placeholder) {
421
449
  return `
@@ -458,7 +486,8 @@ File metadata:
458
486
  - name: ${attachment.name}
459
487
  - storage path: ${attachment.storagePath}
460
488
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
461
- ` : "") + `
489
+ ` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
490
+
462
491
  Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest row/window already saved). The reference ALONE is the whole query: it returns every record written from this file across ALL tables and ALL access groups, so do NOT add table_name or access_group to narrow it. The response is PAGED, so keep fetching pages until it reports there are no more, and take the furthest point from the WHOLE set, never from the first page. Then call readFileContent with the storage path above and a CURSOR that RESUMES just after that point - do NOT start at the beginning. The cursor is derivable from what you already saved:
463
492
  - Spreadsheet: the cursor is "<sheetIndex>:<nextRow>" (0-based sheet index, 1-based row). If you saved up to row R of sheet S, use cursor="S:R+1".
464
493
  - Text: the cursor is the character offset already read.
@@ -1444,6 +1473,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1444
1473
  var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
1445
1474
  var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
1446
1475
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
1476
+ function mcpEndpointFor(anonymous, publicProjectId, service) {
1477
+ if (!anonymous) return { url: mcpUrl(), token: "$ACCESS_TOKEN" };
1478
+ const project = publicProjectId || service;
1479
+ return { url: String(mcpUrl()).replace(/\/+$/, "") + "/p/" + project };
1480
+ }
1447
1481
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
1448
1482
  var VARIANT_IMAGE_DETAIL = "original";
1449
1483
  var VARIANT_TEXT_VERBOSITY = "high";
@@ -1669,7 +1703,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1669
1703
  }
1670
1704
  });
1671
1705
  }
1672
- async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
1706
+ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError, mcpScope) {
1707
+ const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
1673
1708
  return callClaudeWithMcp({
1674
1709
  prompt,
1675
1710
  messages,
@@ -1683,11 +1718,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1683
1718
  fileUrls,
1684
1719
  mcpServer: {
1685
1720
  name: MCP_NAME,
1686
- url: mcpUrl(),
1687
- authorizationToken: "$ACCESS_TOKEN"
1721
+ url: endpoint.url,
1722
+ // Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
1723
+ // guard below drops the key rather than sending an empty one.
1724
+ authorizationToken: endpoint.token
1688
1725
  }});
1689
1726
  }
1690
- async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
1727
+ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError, mcpScope) {
1728
+ const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
1691
1729
  const resolvedModel = model || DEFAULT_OPENAI_MODEL;
1692
1730
  const imageDetail = getOpenAIImageDetail(resolvedModel);
1693
1731
  const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
@@ -1730,11 +1768,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1730
1768
  {
1731
1769
  type: "mcp",
1732
1770
  server_label: MCP_NAME,
1733
- server_url: mcpUrl(),
1771
+ server_url: endpoint.url,
1734
1772
  require_approval: "never",
1735
- headers: {
1736
- Authorization: "Bearer $ACCESS_TOKEN"
1737
- }
1773
+ // No `headers` at all for an anonymous turn: `Bearer ` with an
1774
+ // empty token is a credential the MCP server rejects, and the
1775
+ // project-scoped endpoint needs none.
1776
+ ...endpoint.token ? { headers: { Authorization: "Bearer " + endpoint.token } } : {}
1738
1777
  },
1739
1778
  ...[
1740
1779
  {
@@ -1845,7 +1884,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1845
1884
  // by the tools' schema pattern.
1846
1885
  projectId: info.publicProjectId || service,
1847
1886
  serviceName: info.serviceName,
1848
- serviceDescription: info.serviceDescription
1887
+ serviceDescription: info.serviceDescription,
1888
+ // Per-FILE, not per-project: one project holds public and private files at
1889
+ // once, so this travels on the attachment rather than the identity.
1890
+ accessGroup: attachment.accessGroup
1849
1891
  });
1850
1892
  if (platform === "openai") {
1851
1893
  const resolvedModel2 = info.model || DEFAULT_OPENAI_MODEL;
@@ -3368,10 +3410,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3368
3410
  this._lidSeq += 1;
3369
3411
  return "lid_" + this._lidSeq;
3370
3412
  }
3413
+ /**
3414
+ * The key every per-chat cache hangs off: the restored message cache, the
3415
+ * hydrated-body memo, the live-index key and the per-file storage-path key.
3416
+ *
3417
+ * It carries the IDENTITY as well as the project and platform. A single
3418
+ * browser can hold more than one conversation on one project without a
3419
+ * reload — an anonymous visitor who signs in, or a dashboard user who logs
3420
+ * out and back in as someone else — and with an identity-free key the
3421
+ * previous conversation stayed in the cache and was re-rendered, and written
3422
+ * back, as the new one's. `userId` is the same value the request queue is
3423
+ * named after, so two identities that share a queue share a cache, which is
3424
+ * exactly right.
3425
+ */
3371
3426
  getHistoryCacheKey() {
3372
3427
  var id = this.host.getIdentity();
3373
3428
  if (!id.projectId || id.platform === "none") return "";
3374
- return id.projectId + "#" + id.platform;
3429
+ return id.projectId + "#" + id.platform + "#" + (id.userId || "");
3375
3430
  }
3376
3431
  /** Re-apply memoized hydrated texts onto freshly-mapped messages. Both
3377
3432
  * clients call this right after their mapper runs (loadHistory does it
@@ -3574,7 +3629,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3574
3629
  if (projectId === void 0) projectId = id.projectId;
3575
3630
  if (owner === void 0) owner = id.owner;
3576
3631
  }
3577
- return platform === "openai" ? callOpenAIWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls);
3632
+ var liveId = this.host.getIdentity();
3633
+ var mcpScope = { anonymous: liveId.anonymous, publicProjectId: liveId.publicProjectId };
3634
+ return platform === "openai" ? callOpenAIWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls, void 0, void 0, mcpScope) : callClaudeWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls, void 0, void 0, mcpScope);
3578
3635
  }
3579
3636
  dispatchAgentRequest(params) {
3580
3637
  var self = this;
@@ -5962,6 +6019,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5962
6019
  })).catch(function() {
5963
6020
  });
5964
6021
  });
6022
+ var accessGroup;
6023
+ preIndex = preIndex.then(function() {
6024
+ if (alreadyIndexing) return;
6025
+ if (typeof self.host.uploadAccessGroup !== "function") return;
6026
+ return Promise.resolve(self.host.uploadAccessGroup(member.storagePath)).then(function(g) {
6027
+ accessGroup = g || void 0;
6028
+ }).catch(function() {
6029
+ });
6030
+ });
5965
6031
  return preIndex.then(function() {
5966
6032
  return parseAttachmentContent(member.file, member.file.name, mime || void 0);
5967
6033
  }).then(function(parsedContent) {
@@ -5980,7 +6046,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5980
6046
  storagePath: member.storagePath,
5981
6047
  mime: mime || void 0,
5982
6048
  size: member.file.size,
5983
- url
6049
+ url,
6050
+ accessGroup
5984
6051
  },
5985
6052
  parsedContent: parsedContent || void 0
5986
6053
  }).then(function(ack) {
@@ -6107,6 +6174,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6107
6174
  };
6108
6175
 
6109
6176
  // src/engine/indexing_groups.ts
6177
+ function canonIndexKey(s) {
6178
+ return typeof s === "string" && s ? s.trim() : "";
6179
+ }
6110
6180
  var RUN_RECORD_WORKING_STALE_MS = 6 * 60 * 60 * 1e3;
6111
6181
  var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
6112
6182
  var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
@@ -6176,15 +6246,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6176
6246
  var openRunOfKey = {};
6177
6247
  var runsOfKey = {};
6178
6248
  var keyOfRun = {};
6249
+ var newestTsOfRun = {};
6179
6250
  var runSeq = 0;
6180
6251
  for (var i = 0; i < list.length; i++) {
6181
6252
  var msg = list[i];
6182
6253
  if (!msg || !msg.isBackgroundTask) continue;
6183
6254
  var runId;
6184
6255
  var ref = msg.role === "user" ? readFileRef(msg) : null;
6185
- if (ref) {
6256
+ if (msg._serverItemId && runByItemId[msg._serverItemId]) {
6257
+ runId = runByItemId[msg._serverItemId];
6258
+ } else if (ref) {
6186
6259
  var key = ref.path || keyByName[ref.name] || ref.name;
6187
- if (!ref.continued && openRunOfKey[key]) delete openRunOfKey[key];
6260
+ var alreadySeen = !!(msg._serverItemId && runByItemId[msg._serverItemId]);
6261
+ var openId = openRunOfKey[key];
6262
+ var notLater = false;
6263
+ if (openId && typeof msg._ts === "number" && typeof newestTsOfRun[openId] === "number") {
6264
+ notLater = msg._ts <= newestTsOfRun[openId];
6265
+ }
6266
+ if (!ref.continued && !alreadySeen && !notLater && openRunOfKey[key]) delete openRunOfKey[key];
6188
6267
  runId = openRunOfKey[key];
6189
6268
  if (!runId) {
6190
6269
  runId = "run" + runSeq++;
@@ -6192,12 +6271,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6192
6271
  keyOfRun[runId] = key;
6193
6272
  (runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
6194
6273
  }
6195
- } else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
6196
- runId = runByItemId[msg._serverItemId];
6197
6274
  } else if (msg.role !== "user") {
6198
6275
  runId = runOfIndex[i - 1];
6199
6276
  }
6200
6277
  if (!runId) continue;
6278
+ if (typeof msg._ts === "number") {
6279
+ var prevTs = newestTsOfRun[runId];
6280
+ if (typeof prevTs !== "number" || msg._ts > prevTs) newestTsOfRun[runId] = msg._ts;
6281
+ }
6201
6282
  var g = groups[runId];
6202
6283
  if (!g) {
6203
6284
  var fileKey = keyOfRun[runId];
@@ -6362,20 +6443,33 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6362
6443
  for (var ci = 0; ci < order.length; ci++) {
6363
6444
  var cg = groups[order[ci]];
6364
6445
  if (cg.path) {
6365
- coveredPaths[cg.path] = true;
6366
- if (cg.key) coveredPaths[cg.key] = true;
6367
- } else if (cg.name) coveredPathlessNames[cg.name] = true;
6368
- else if (cg.key) coveredPaths[cg.key] = true;
6446
+ coveredPaths[canonIndexKey(cg.path)] = true;
6447
+ if (cg.key) coveredPaths[canonIndexKey(cg.key)] = true;
6448
+ } else if (cg.name) coveredPathlessNames[canonIndexKey(cg.name)] = true;
6449
+ else if (cg.key) coveredPaths[canonIndexKey(cg.key)] = true;
6369
6450
  }
6370
6451
  var now = opts && typeof opts.now === "number" ? opts.now : Date.now();
6371
6452
  var stubClearedAt = opts && typeof opts.stubClearedAt === "number" && opts.stubClearedAt > 0 ? opts.stubClearedAt : 0;
6372
6453
  for (var sp in runStubs) {
6373
6454
  var rec = runStubs[sp];
6374
- if (!sp || !rec || !rec.status || coveredPaths[sp]) continue;
6455
+ if (!sp || !rec || !rec.status || coveredPaths[canonIndexKey(sp)]) continue;
6375
6456
  var fname = rec.filename || sp.split("/").pop() || sp;
6376
- if (coveredPathlessNames[fname]) continue;
6457
+ if (coveredPathlessNames[canonIndexKey(fname)]) continue;
6377
6458
  if (stubPlatform && rec.platform && rec.platform !== stubPlatform) continue;
6378
- var live = !!liveIndexKeys[sp] || !!liveIndexKeys[fname];
6459
+ var live = !!liveIndexKeys[sp] || !!liveIndexKeys[canonIndexKey(sp)];
6460
+ if (!live && (liveIndexKeys[fname] || liveIndexKeys[canonIndexKey(fname)])) {
6461
+ var claimedByOther = false;
6462
+ for (var lk in liveIndexKeys) {
6463
+ if (!liveIndexKeys[lk]) continue;
6464
+ var lkc = canonIndexKey(lk);
6465
+ if (lkc === canonIndexKey(sp)) continue;
6466
+ if (lkc.length > fname.length && lkc.slice(-(fname.length + 1)) === "/" + fname) {
6467
+ claimedByOther = true;
6468
+ break;
6469
+ }
6470
+ }
6471
+ live = !claimedByOther;
6472
+ }
6379
6473
  var recWhen = typeof rec.finished === "number" ? rec.finished : typeof rec.started === "number" ? rec.started : void 0;
6380
6474
  if (stubClearedAt && !live && recWhen !== void 0 && recWhen <= stubClearedAt) continue;
6381
6475
  var st = "active";
@@ -6383,7 +6477,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6383
6477
  var res = false;
6384
6478
  var reason;
6385
6479
  if (!live) {
6386
- if (rec.status === "done" || doneKeys[sp] || doneKeys[fname]) {
6480
+ if (rec.status === "done" || doneKeys[sp] || doneKeys[canonIndexKey(sp)] || doneKeys[fname] || doneKeys[canonIndexKey(fname)]) {
6387
6481
  st = "done";
6388
6482
  fin = true;
6389
6483
  } else if (rec.status === "error") {
@@ -6437,11 +6531,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6437
6531
  }
6438
6532
  }
6439
6533
  var suppressAnchor = {};
6534
+ var stubByCanon = {};
6535
+ if (runStubs) for (var ck in runStubs) stubByCanon[canonIndexKey(ck)] = runStubs[ck];
6440
6536
  if (runStubs) {
6441
6537
  for (var ti2 = 0; ti2 < order.length; ti2++) {
6442
6538
  var tg = groups[order[ti2]];
6443
6539
  if (!newestRunOfKey[order[ti2]]) continue;
6444
- var trec = tg.path && runStubs[tg.path] || runStubs[tg.key];
6540
+ var trec = tg.path && (runStubs[tg.path] || stubByCanon[canonIndexKey(tg.path)]) || runStubs[tg.key] || stubByCanon[canonIndexKey(tg.key)];
6445
6541
  if (!trec || typeof trec.started !== "number") continue;
6446
6542
  if (stubPlatform && trec.platform && trec.platform !== stubPlatform) continue;
6447
6543
  suppressAnchor[order[ti2]] = true;
@@ -6482,7 +6578,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6482
6578
  (function() {
6483
6579
  var MCP_PROD = "https://mcp.broadwayinc.computer";
6484
6580
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
6485
- var BQ_VERSION = "1.8.18" ;
6581
+ var BQ_VERSION = "1.9.4" ;
6486
6582
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
6487
6583
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
6488
6584
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -6498,7 +6594,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6498
6594
  // sessionStorage
6499
6595
  googleRedirect: "bq_embed:google_redirect",
6500
6596
  // sessionStorage
6501
- clearHorizon: "bq_embed:clearedAt"
6597
+ clearHorizon: "bq_embed:clearedAt",
6598
+ anonId: "bq_embed:anon_id"
6599
+ // per-project anonymous device id
6502
6600
  };
6503
6601
  function h(tag, attrs) {
6504
6602
  var el = document.createElement(tag);
@@ -6653,6 +6751,40 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6653
6751
  function skey(base) {
6654
6752
  return base + ":" + (S.projectId || "default");
6655
6753
  }
6754
+ function anonymousAllowed() {
6755
+ if (S.opts && typeof S.opts.allowAnonymous === "boolean") return S.opts.allowAnonymous;
6756
+ var conf = S.service && S.service.conf || null;
6757
+ if (!conf || typeof conf.prevent_anonymous === "undefined") return false;
6758
+ return !conf.prevent_anonymous;
6759
+ }
6760
+ function isAnonymousSession() {
6761
+ return !S.user && anonymousAllowed();
6762
+ }
6763
+ function randomId() {
6764
+ try {
6765
+ var buf = new Uint8Array(16);
6766
+ (window.crypto || window.msCrypto).getRandomValues(buf);
6767
+ var out = "";
6768
+ for (var i = 0; i < buf.length; i++) out += ("0" + buf[i].toString(16)).slice(-2);
6769
+ return out;
6770
+ } catch (e) {
6771
+ return "x" + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
6772
+ }
6773
+ }
6774
+ var _anonIdMemo = null;
6775
+ function anonDeviceId() {
6776
+ if (_anonIdMemo) return _anonIdMemo;
6777
+ var key = skey(SK.anonId);
6778
+ var stored = lsGet(key);
6779
+ if (stored) {
6780
+ _anonIdMemo = stored;
6781
+ return stored;
6782
+ }
6783
+ var minted = "anon_" + randomId();
6784
+ lsSet(key, minted);
6785
+ _anonIdMemo = lsGet(key) || minted;
6786
+ return _anonIdMemo;
6787
+ }
6656
6788
  function loadTheme() {
6657
6789
  var stored = lsGet(SK.theme);
6658
6790
  if (stored === "dark" || stored === "light") return stored;
@@ -7181,6 +7313,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7181
7313
  text: "Sign up \u2192"
7182
7314
  }));
7183
7315
  }
7316
+ var canReturnToChat = !S.user && anonymousAllowed();
7184
7317
  var form = h(
7185
7318
  "form",
7186
7319
  { class: "bq-form", onsubmit: submit },
@@ -7190,7 +7323,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7190
7323
  errorBox,
7191
7324
  h("div", { class: "bq-form-bottom" }, submitBtn)
7192
7325
  );
7193
- var children = authHeader("Login").concat([form]);
7326
+ var children = [];
7327
+ if (canReturnToChat) {
7328
+ children.push(h(
7329
+ "div",
7330
+ { class: "bq-settings-top" },
7331
+ h("button", {
7332
+ class: "bq-link",
7333
+ type: "button",
7334
+ onclick: function() {
7335
+ enterAfterLogin();
7336
+ },
7337
+ text: "\u2190 Back to chat"
7338
+ })
7339
+ ));
7340
+ }
7341
+ children = children.concat(authHeader("Login")).concat([form]);
7194
7342
  if (googleEnabled()) {
7195
7343
  children.push(
7196
7344
  h(
@@ -8141,7 +8289,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8141
8289
  return void 0;
8142
8290
  })(),
8143
8291
  owner: S.owner,
8144
- userId: S.user && S.user.user_id || S.projectId,
8292
+ // The chat identity, which the engine turns into the request queue
8293
+ // name. An anonymous visitor gets their DEVICE id rather than the
8294
+ // project id: the old fallback gave every anonymous visitor of a
8295
+ // project the same queue, so they would have shared one transcript
8296
+ // and head-of-line-blocked each other's turns on a single FIFO.
8297
+ userId: S.user && S.user.user_id || (isAnonymousSession() ? anonDeviceId() : S.projectId),
8298
+ // Sends the turn's MCP tools to the project-scoped, credential-free
8299
+ // endpoint instead of the root one with an empty bearer.
8300
+ anonymous: isAnonymousSession(),
8145
8301
  platform: S.aiPlatform,
8146
8302
  model: S.aiModel || void 0,
8147
8303
  serviceName: S.serviceName,
@@ -8203,7 +8359,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8203
8359
  return deleteFileIndexRecordDb(path);
8204
8360
  },
8205
8361
  ensureFileIndexRecord: function(path, meta) {
8206
- return ensureFileIndexRecordDb(path, meta);
8362
+ return resolveUploadAccessGroup(path).then(function(g) {
8363
+ return ensureFileIndexRecordDb(path, meta, g);
8364
+ });
8365
+ },
8366
+ uploadAccessGroup: function(path) {
8367
+ return resolveUploadAccessGroup(path);
8207
8368
  },
8208
8369
  storagePathFor: function(relPath) {
8209
8370
  return attachmentStoragePath(relPath);
@@ -8215,7 +8376,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8215
8376
  return promptOverwrite(filename);
8216
8377
  },
8217
8378
  resetOverwriteBatch: function() {
8218
- return resetOverwriteBatch();
8379
+ resetOverwriteBatch();
8380
+ resetAccessGroupBatch();
8219
8381
  },
8220
8382
  renderAttachmentChips: function() {
8221
8383
  renderAttachmentChips();
@@ -8326,7 +8488,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8326
8488
  // paints), so the model is told what it opened with. Same call as
8327
8489
  // buildGreetingEl, so the two can never disagree.
8328
8490
  greeting: greetingParts().text,
8329
- canUpload: !uploadsFrozenForUser()});
8491
+ canUpload: !uploadsFrozenForUser(),
8492
+ // Where THIS project's indexer writes. The MCP's auto-fill assumes
8493
+ // "authorized"; on a project set to public or private that would
8494
+ // search the wrong group and answer "nothing found".
8495
+ indexAccessGroup: projectUploadAccessGroup()});
8330
8496
  }
8331
8497
  function refreshSkapiSession() {
8332
8498
  return S.skapi.getProfile({ refreshToken: true }).then(function() {
@@ -8769,12 +8935,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8769
8935
  }
8770
8936
  return createChain();
8771
8937
  }
8772
- function ensureFileIndexRecordDb(storagePath, meta) {
8938
+ function ensureFileIndexRecordDb(storagePath, meta, accessGroup) {
8773
8939
  if (!storagePath || !S.skapi || typeof S.skapi.postRecord !== "function") return Promise.resolve();
8774
8940
  return Promise.resolve(S.skapi.postRecord(null, {
8775
8941
  service: S.projectId,
8776
8942
  unique_id: "src::" + storagePath,
8777
- table: { name: "file_summaries", access_group: "authorized" },
8943
+ table: { name: "file_summaries", access_group: normalizeUploadAccessGroup(accessGroup) },
8778
8944
  // Deleting the file record must cascade to every record referencing it.
8779
8945
  source: { can_remove_referencing_records: true },
8780
8946
  data: {
@@ -9473,7 +9639,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9473
9639
  }
9474
9640
  function getClearHistoryStorageKey() {
9475
9641
  if (!S.projectId || S.aiPlatform === "none") return "";
9476
- return SK.clearHorizon + ":" + S.projectId + "#" + S.aiPlatform;
9642
+ var key = SK.clearHorizon + ":" + S.projectId + "#" + S.aiPlatform;
9643
+ if (isAnonymousSession()) key += "#" + anonDeviceId();
9644
+ return key;
9477
9645
  }
9478
9646
  function getClearedAt() {
9479
9647
  var key = getClearHistoryStorageKey();
@@ -9837,7 +10005,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9837
10005
  var liveIndex = session.getLiveIndexState();
9838
10006
  var fresh = markerSweep.svc === S.projectId;
9839
10007
  var stubs = void 0;
9840
- if (fresh) {
10008
+ if (fresh && !isAnonymousSession()) {
9841
10009
  stubs = {};
9842
10010
  var myId = S.user && S.user.user_id || "";
9843
10011
  for (var rp in markerSweep.runs) {
@@ -10348,6 +10516,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10348
10516
  }
10349
10517
  });
10350
10518
  CS.settingsBtnEl = settingsBtn;
10519
+ var headerRight = isAnonymousSession() ? h("button", {
10520
+ class: "bq-link",
10521
+ type: "button",
10522
+ title: "Login",
10523
+ onclick: function() {
10524
+ renderLogin();
10525
+ },
10526
+ text: "Login"
10527
+ }) : settingsBtn;
10528
+ if (isAnonymousSession()) CS.settingsBtnEl = null;
10351
10529
  var header = h(
10352
10530
  "div",
10353
10531
  { class: "bq-section-title" },
@@ -10355,7 +10533,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10355
10533
  "div",
10356
10534
  { class: "bq-title-row" },
10357
10535
  brandTitleEl(),
10358
- h("div", { class: "bq-title-right" }, settingsBtn)
10536
+ h("div", { class: "bq-title-right" }, headerRight)
10359
10537
  )
10360
10538
  );
10361
10539
  var chatArea;
@@ -10492,6 +10670,128 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10492
10670
  overwriteState.resolver = null;
10493
10671
  if (r) r(choice);
10494
10672
  }
10673
+ var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
10674
+ var UPLOAD_ACCESS_LABELS = {
10675
+ public: "Public",
10676
+ authorized: "Signed in users",
10677
+ private: "Only me"
10678
+ };
10679
+ var UPLOAD_ACCESS_HINTS = {
10680
+ public: "Anyone can ask about this file, including visitors who are not logged in.",
10681
+ authorized: "Only users signed in to this project can ask about this file.",
10682
+ private: "Only you can ask about this file."
10683
+ };
10684
+ function normalizeUploadAccessGroup(v) {
10685
+ return UPLOAD_ACCESS_GROUPS.indexOf(v) === -1 ? "authorized" : v;
10686
+ }
10687
+ function projectAccessSetting() {
10688
+ var conf = S.service && S.service.conf || {};
10689
+ var v = conf.default_access_group;
10690
+ if (v === "ask") return "ask";
10691
+ return UPLOAD_ACCESS_GROUPS.indexOf(v) === -1 ? null : v;
10692
+ }
10693
+ function projectUploadAccessGroup() {
10694
+ var v = projectAccessSetting();
10695
+ return v && v !== "ask" ? v : "authorized";
10696
+ }
10697
+ function projectAsksUploadAccess() {
10698
+ return projectAccessSetting() === "ask";
10699
+ }
10700
+ var accessGroupState = { resolver: null, sticky: null, handle: null, applyToAll: false, choice: "authorized", perPath: {} };
10701
+ function resetAccessGroupBatch() {
10702
+ accessGroupState.sticky = null;
10703
+ accessGroupState.applyToAll = false;
10704
+ accessGroupState.perPath = {};
10705
+ }
10706
+ function chooseAccessGroup(choice) {
10707
+ var picked = normalizeUploadAccessGroup(choice);
10708
+ if (accessGroupState.applyToAll) accessGroupState.sticky = picked;
10709
+ if (accessGroupState.handle) {
10710
+ accessGroupState.handle.close();
10711
+ accessGroupState.handle = null;
10712
+ }
10713
+ var r = accessGroupState.resolver;
10714
+ accessGroupState.resolver = null;
10715
+ if (r) r(picked);
10716
+ }
10717
+ var accessGroupChain = Promise.resolve();
10718
+ function resolveUploadAccessGroup(storagePath) {
10719
+ if (!projectAsksUploadAccess()) return Promise.resolve(projectUploadAccessGroup());
10720
+ var fallback = projectUploadAccessGroup();
10721
+ if (accessGroupState.sticky) return Promise.resolve(accessGroupState.sticky);
10722
+ var pathKey = String(storagePath || "");
10723
+ if (pathKey && accessGroupState.perPath[pathKey]) {
10724
+ return Promise.resolve(accessGroupState.perPath[pathKey]);
10725
+ }
10726
+ var run = accessGroupChain.then(function() {
10727
+ if (accessGroupState.sticky) return accessGroupState.sticky;
10728
+ if (pathKey && accessGroupState.perPath[pathKey]) {
10729
+ return accessGroupState.perPath[pathKey];
10730
+ }
10731
+ accessGroupState.applyToAll = false;
10732
+ accessGroupState.choice = fallback;
10733
+ var filename = String(storagePath || "").split("/").pop() || "this file";
10734
+ return new Promise(function(resolve) {
10735
+ accessGroupState.resolver = resolve;
10736
+ accessGroupState.handle = openModal(function() {
10737
+ var list = h("div", { class: "bq-access-options" });
10738
+ UPLOAD_ACCESS_GROUPS.forEach(function(g) {
10739
+ var input = h("input", { type: "radio", name: "bq-access-group", value: g });
10740
+ input.checked = g === accessGroupState.choice;
10741
+ input.addEventListener("change", function() {
10742
+ if (input.checked) accessGroupState.choice = g;
10743
+ });
10744
+ list.appendChild(h(
10745
+ "label",
10746
+ { class: "bq-access-option" },
10747
+ input,
10748
+ h("span", { class: "bq-access-option-label", text: UPLOAD_ACCESS_LABELS[g] }),
10749
+ h("span", { class: "bq-access-option-hint", text: UPLOAD_ACCESS_HINTS[g] })
10750
+ ));
10751
+ });
10752
+ var applyCb = h("input", { type: "checkbox" });
10753
+ applyCb.addEventListener("change", function() {
10754
+ accessGroupState.applyToAll = !!applyCb.checked;
10755
+ });
10756
+ var applyLabel = h(
10757
+ "label",
10758
+ { class: "bq-overwrite-applyall" },
10759
+ applyCb,
10760
+ h("span", { text: "Apply to all remaining files" })
10761
+ );
10762
+ return h(
10763
+ "div",
10764
+ { class: "bq-modal" },
10765
+ h("div", { class: "bq-modal-delete-header" }, h("span", { text: "Who can read this file?" })),
10766
+ h(
10767
+ "p",
10768
+ { class: "bq-modal-desc" },
10769
+ "Choose who can ask questions about \u201C" + filename + "\u201D once it is indexed."
10770
+ ),
10771
+ list,
10772
+ applyLabel,
10773
+ h(
10774
+ "div",
10775
+ { class: "bq-modal-btns" },
10776
+ h("button", { class: "btn", type: "button", onclick: function() {
10777
+ chooseAccessGroup(accessGroupState.choice);
10778
+ } }, "Upload")
10779
+ )
10780
+ );
10781
+ }, { dismissible: false });
10782
+ });
10783
+ });
10784
+ accessGroupChain = run.catch(function() {
10785
+ return void 0;
10786
+ });
10787
+ return run.then(function(picked) {
10788
+ var g = normalizeUploadAccessGroup(picked);
10789
+ if (pathKey) accessGroupState.perPath[pathKey] = g;
10790
+ return g;
10791
+ }).catch(function() {
10792
+ return fallback;
10793
+ });
10794
+ }
10495
10795
  function promptOverwrite(filename) {
10496
10796
  if (overwriteState.sticky) return Promise.resolve(overwriteState.sticky);
10497
10797
  overwriteState.applyToAll = false;
@@ -10603,6 +10903,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10603
10903
  }).catch(function() {
10604
10904
  }).then(function() {
10605
10905
  S.user = null;
10906
+ if (anonymousAllowed()) return enterAfterLogin();
10606
10907
  renderLogin();
10607
10908
  });
10608
10909
  }
@@ -10616,7 +10917,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10616
10917
  }).then(function() {
10617
10918
  return loadServiceInfo();
10618
10919
  }).then(function(conn) {
10619
- S.service = conn;
10920
+ if (conn) S.service = conn;
10620
10921
  applyAgentConfig();
10621
10922
  }).then(function() {
10622
10923
  renderChat();
@@ -10676,6 +10977,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10676
10977
  return getProfile().then(function(user) {
10677
10978
  S.user = user;
10678
10979
  if (!user) {
10980
+ if (anonymousAllowed()) return enterAfterLogin();
10679
10981
  renderLogin();
10680
10982
  return;
10681
10983
  }
@@ -10714,8 +11016,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10714
11016
  // defaults to current host page
10715
11017
  hostDomain: null,
10716
11018
  // db-CDN host; null → skapi.app (dev) / skapi.com (prod)
10717
- attachmentParsers: null
11019
+ attachmentParsers: null,
10718
11020
  // client-side attachment parsers, e.g. [createHwpParser()]
11021
+ // Open the chat with no login for visitors without an account.
11022
+ // null → follow the project's own "Allow anonymous users" setting
11023
+ // (getConnectionInfo().conf.prevent_anonymous); true/false pins it.
11024
+ allowAnonymous: null,
11025
+ // Server-driven windowed indexing; read at configureChatEngine time.
11026
+ // Listed here so the defaults object is the full opt surface.
11027
+ windowedIndexing: true
10719
11028
  }, opts || {});
10720
11029
  S.mountEl = mountEl;
10721
11030
  clear(mountEl);