bunnyquery 1.9.1 → 1.9.5
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.css +33 -0
- package/bunnyquery.js +331 -41
- package/dist/engine.cjs +107 -27
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +147 -3
- package/dist/engine.d.ts +147 -3
- package/dist/engine.mjs +105 -28
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/history.ts +111 -5
- package/src/engine/host.ts +29 -0
- package/src/engine/index.ts +5 -0
- package/src/engine/prompts/chat_system_prompt.ts +20 -1
- package/src/engine/prompts/index.ts +1 -0
- package/src/engine/prompts/indexing_system_prompt.ts +13 -1
- package/src/engine/prompts/indexing_user_message.ts +29 -2
- package/src/engine/requests.ts +57 -6
- package/src/engine/session.ts +59 -11
- package/src/widget.css +33 -0
package/bunnyquery.js
CHANGED
|
@@ -292,11 +292,13 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
|
|
|
292
292
|
// src/engine/prompts/chat_system_prompt.ts
|
|
293
293
|
function buildChatSystemPrompt(params) {
|
|
294
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"';
|
|
295
297
|
let systemPrompt = `
|
|
296
298
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
297
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.
|
|
298
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.
|
|
299
|
-
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"
|
|
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.
|
|
300
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.
|
|
301
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.
|
|
302
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.
|
|
@@ -345,6 +347,7 @@ Project description: """${serviceDescription}"""`;
|
|
|
345
347
|
// src/engine/prompts/indexing_system_prompt.ts
|
|
346
348
|
function buildIndexingSystemPrompt(params) {
|
|
347
349
|
const { projectId, serviceName, serviceDescription } = params;
|
|
350
|
+
const accessGroup = params.accessGroup === "public" || params.accessGroup === "private" ? params.accessGroup : "authorized";
|
|
348
351
|
let systemPrompt = `You are a background indexing agent for project ${projectId}.
|
|
349
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.
|
|
350
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.
|
|
@@ -353,7 +356,8 @@ Project description: """${serviceDescription}"""`;
|
|
|
353
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).
|
|
354
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.
|
|
355
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).
|
|
356
|
-
- 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 "
|
|
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.
|
|
357
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.
|
|
358
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.
|
|
359
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.
|
|
@@ -374,6 +378,10 @@ Project description: """${serviceDescription}"""`;
|
|
|
374
378
|
}
|
|
375
379
|
|
|
376
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
|
+
}
|
|
377
385
|
function buildIndexingUserMessage(attachment, options) {
|
|
378
386
|
const head = `A new file has just been uploaded. Index it now.
|
|
379
387
|
|
|
@@ -382,7 +390,11 @@ File metadata:
|
|
|
382
390
|
- storage path: ${attachment.storagePath}
|
|
383
391
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
384
392
|
` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
|
|
385
|
-
` : "")
|
|
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
|
+
`;
|
|
386
398
|
if (options?.inlineContent) {
|
|
387
399
|
return head + `
|
|
388
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.
|
|
@@ -430,7 +442,8 @@ Records for the earlier pages are ALREADY saved (they reference "${src}"). The N
|
|
|
430
442
|
- name: ${attachment.name}
|
|
431
443
|
- storage path: ${attachment.storagePath}
|
|
432
444
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
433
|
-
` : "")
|
|
445
|
+
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
446
|
+
`;
|
|
434
447
|
}
|
|
435
448
|
function buildRenderDatafy(placeholder) {
|
|
436
449
|
return `
|
|
@@ -473,7 +486,8 @@ File metadata:
|
|
|
473
486
|
- name: ${attachment.name}
|
|
474
487
|
- storage path: ${attachment.storagePath}
|
|
475
488
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
476
|
-
` : "") +
|
|
489
|
+
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
490
|
+
|
|
477
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:
|
|
478
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".
|
|
479
493
|
- Text: the cursor is the character offset already read.
|
|
@@ -1459,6 +1473,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1459
1473
|
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
|
|
1460
1474
|
var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
|
|
1461
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
|
+
}
|
|
1462
1481
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
1463
1482
|
var VARIANT_IMAGE_DETAIL = "original";
|
|
1464
1483
|
var VARIANT_TEXT_VERBOSITY = "high";
|
|
@@ -1684,7 +1703,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1684
1703
|
}
|
|
1685
1704
|
});
|
|
1686
1705
|
}
|
|
1687
|
-
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);
|
|
1688
1708
|
return callClaudeWithMcp({
|
|
1689
1709
|
prompt,
|
|
1690
1710
|
messages,
|
|
@@ -1698,11 +1718,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1698
1718
|
fileUrls,
|
|
1699
1719
|
mcpServer: {
|
|
1700
1720
|
name: MCP_NAME,
|
|
1701
|
-
url:
|
|
1702
|
-
authorizationToken
|
|
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
|
|
1703
1725
|
}});
|
|
1704
1726
|
}
|
|
1705
|
-
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);
|
|
1706
1729
|
const resolvedModel = model || DEFAULT_OPENAI_MODEL;
|
|
1707
1730
|
const imageDetail = getOpenAIImageDetail(resolvedModel);
|
|
1708
1731
|
const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
|
|
@@ -1745,11 +1768,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1745
1768
|
{
|
|
1746
1769
|
type: "mcp",
|
|
1747
1770
|
server_label: MCP_NAME,
|
|
1748
|
-
server_url:
|
|
1771
|
+
server_url: endpoint.url,
|
|
1749
1772
|
require_approval: "never",
|
|
1750
|
-
headers:
|
|
1751
|
-
|
|
1752
|
-
|
|
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 } } : {}
|
|
1753
1777
|
},
|
|
1754
1778
|
...[
|
|
1755
1779
|
{
|
|
@@ -1860,7 +1884,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1860
1884
|
// by the tools' schema pattern.
|
|
1861
1885
|
projectId: info.publicProjectId || service,
|
|
1862
1886
|
serviceName: info.serviceName,
|
|
1863
|
-
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
|
|
1864
1891
|
});
|
|
1865
1892
|
if (platform === "openai") {
|
|
1866
1893
|
const resolvedModel2 = info.model || DEFAULT_OPENAI_MODEL;
|
|
@@ -2177,6 +2204,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2177
2204
|
const fetch2 = getChatHistory;
|
|
2178
2205
|
const bgQueue = bgIndexingQueueName(params.userId, params.service);
|
|
2179
2206
|
const base = { service: params.service, owner: params.owner, platform: params.platform };
|
|
2207
|
+
const surfaceScope = params.scopeSurfaceToQueue && params.userId ? { queue: params.userId, queue_exact: true } : { queue_exclude: bgQueue };
|
|
2180
2208
|
const fetchMore = !!(fetchOptions && fetchOptions.fetchMore);
|
|
2181
2209
|
const limit = fetchOptions && fetchOptions.limit;
|
|
2182
2210
|
const firstLoad = !splitHistoryStates[key];
|
|
@@ -2204,13 +2232,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2204
2232
|
} else {
|
|
2205
2233
|
const sOpts = { fetchMore };
|
|
2206
2234
|
if (limit) sOpts.limit = limit;
|
|
2207
|
-
let s = await fetch2({ ...base,
|
|
2235
|
+
let s = await fetch2({ ...base, ...surfaceScope }, sOpts);
|
|
2208
2236
|
let hops = 0;
|
|
2209
2237
|
while (s && !s.endOfList && !(s.list || []).length && hops < SURFACE_EMPTY_MAX_PAGES) {
|
|
2210
2238
|
hops++;
|
|
2211
2239
|
const nOpts = { fetchMore: true };
|
|
2212
2240
|
if (limit) nOpts.limit = limit;
|
|
2213
|
-
s = await fetch2({ ...base,
|
|
2241
|
+
s = await fetch2({ ...base, ...surfaceScope }, nOpts);
|
|
2214
2242
|
}
|
|
2215
2243
|
state.pendingSurface = {
|
|
2216
2244
|
list: s && Array.isArray(s.list) ? s.list : [],
|
|
@@ -2343,6 +2371,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2343
2371
|
firstLoad
|
|
2344
2372
|
};
|
|
2345
2373
|
}
|
|
2374
|
+
function chatCacheKey(projectId, platform, userId) {
|
|
2375
|
+
if (!projectId || platform === "none") return "";
|
|
2376
|
+
return projectId + "#" + platform + "#" + (userId || "");
|
|
2377
|
+
}
|
|
2378
|
+
function indexScopeKey(projectId, platform) {
|
|
2379
|
+
if (!projectId || platform === "none") return "";
|
|
2380
|
+
return projectId + "#" + platform;
|
|
2381
|
+
}
|
|
2346
2382
|
function mapHistoryListToMessages(list, platform, opts) {
|
|
2347
2383
|
var mapped = [], runningItemIds = [];
|
|
2348
2384
|
var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
|
|
@@ -2429,7 +2465,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2429
2465
|
}
|
|
2430
2466
|
});
|
|
2431
2467
|
if (opts.projectId) {
|
|
2432
|
-
var ownerKey = opts.projectId
|
|
2468
|
+
var ownerKey = chatCacheKey(opts.projectId, platform, opts.userId);
|
|
2433
2469
|
for (var oi = 0; oi < mapped.length; oi++) mapped[oi]._ownerKey = ownerKey;
|
|
2434
2470
|
}
|
|
2435
2471
|
return { messages: mapped, runningItemIds };
|
|
@@ -2956,7 +2992,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2956
2992
|
/** Storage paths are project-relative, and one ChatSession serves every
|
|
2957
2993
|
* project, so a claim has to be scoped the way a stop is (_indexKeyOf). */
|
|
2958
2994
|
_indexClaimKey(storagePath) {
|
|
2959
|
-
|
|
2995
|
+
var id = this.host.getIdentity();
|
|
2996
|
+
return indexScopeKey(id.projectId, id.platform) + "|" + storagePath;
|
|
2960
2997
|
}
|
|
2961
2998
|
/**
|
|
2962
2999
|
* Take this file's indexing slot, or report that someone already has it.
|
|
@@ -3383,10 +3420,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3383
3420
|
this._lidSeq += 1;
|
|
3384
3421
|
return "lid_" + this._lidSeq;
|
|
3385
3422
|
}
|
|
3423
|
+
/**
|
|
3424
|
+
* The key every per-chat cache hangs off: the restored message cache, the
|
|
3425
|
+
* hydrated-body memo, the live-index key and the per-file storage-path key.
|
|
3426
|
+
*
|
|
3427
|
+
* It carries the IDENTITY as well as the project and platform. A single
|
|
3428
|
+
* browser can hold more than one conversation on one project without a
|
|
3429
|
+
* reload — an anonymous visitor who signs in, or a dashboard user who logs
|
|
3430
|
+
* out and back in as someone else — and with an identity-free key the
|
|
3431
|
+
* previous conversation stayed in the cache and was re-rendered, and written
|
|
3432
|
+
* back, as the new one's. `userId` is the same value the request queue is
|
|
3433
|
+
* named after, so two identities that share a queue share a cache, which is
|
|
3434
|
+
* exactly right.
|
|
3435
|
+
*/
|
|
3386
3436
|
getHistoryCacheKey() {
|
|
3387
3437
|
var id = this.host.getIdentity();
|
|
3388
|
-
|
|
3389
|
-
return id.projectId + "#" + id.platform;
|
|
3438
|
+
return chatCacheKey(id.projectId, id.platform, id.userId);
|
|
3390
3439
|
}
|
|
3391
3440
|
/** Re-apply memoized hydrated texts onto freshly-mapped messages. Both
|
|
3392
3441
|
* clients call this right after their mapper runs (loadHistory does it
|
|
@@ -3589,7 +3638,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3589
3638
|
if (projectId === void 0) projectId = id.projectId;
|
|
3590
3639
|
if (owner === void 0) owner = id.owner;
|
|
3591
3640
|
}
|
|
3592
|
-
|
|
3641
|
+
var liveId = this.host.getIdentity();
|
|
3642
|
+
var mcpScope = { anonymous: liveId.anonymous, publicProjectId: liveId.publicProjectId };
|
|
3643
|
+
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);
|
|
3593
3644
|
}
|
|
3594
3645
|
dispatchAgentRequest(params) {
|
|
3595
3646
|
var self = this;
|
|
@@ -3934,7 +3985,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3934
3985
|
}
|
|
3935
3986
|
if (stageId) delete this._liveStages[stageId];
|
|
3936
3987
|
var llmComposed = composedForLlm || composed;
|
|
3937
|
-
var key =
|
|
3988
|
+
var key = chatCacheKey(id.projectId, id.platform, id.userId);
|
|
3938
3989
|
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
3939
3990
|
var isQueuedSend = !offChat && (useBgQueue || this.state.sending || this.state.messages.some(function(m) {
|
|
3940
3991
|
return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
|
|
@@ -4500,7 +4551,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4500
4551
|
cancelIndexingGroup(group) {
|
|
4501
4552
|
var self = this;
|
|
4502
4553
|
if (!group || !group.key) return;
|
|
4503
|
-
var
|
|
4554
|
+
var idn = this.host.getIdentity();
|
|
4555
|
+
var scoped = indexScopeKey(idn.projectId, idn.platform) + "|" + group.key;
|
|
4504
4556
|
this.cancelledIndexKeys.add(scoped);
|
|
4505
4557
|
if (!group.finished) {
|
|
4506
4558
|
var stoppedIds = {};
|
|
@@ -4968,7 +5020,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4968
5020
|
if (!entry) return "";
|
|
4969
5021
|
var file = entry.storagePath || entry.filename;
|
|
4970
5022
|
if (!file) return "";
|
|
4971
|
-
return entry.projectId
|
|
5023
|
+
return indexScopeKey(entry.projectId, entry.platform) + "|" + file;
|
|
4972
5024
|
}
|
|
4973
5025
|
/**
|
|
4974
5026
|
* Reconcile the bg queue with the files the user has stopped.
|
|
@@ -5495,7 +5547,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5495
5547
|
loadHistory(fetchMore, token) {
|
|
5496
5548
|
var self = this;
|
|
5497
5549
|
var id = this.host.getIdentity();
|
|
5498
|
-
var loadKey =
|
|
5550
|
+
var loadKey = chatCacheKey(id.projectId, id.platform, id.userId);
|
|
5499
5551
|
if (token === void 0) token = this.state.gateRefreshToken;
|
|
5500
5552
|
if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.projectId) {
|
|
5501
5553
|
return Promise.resolve();
|
|
@@ -5514,7 +5566,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5514
5566
|
if (fetchMore && this.state.historyStartKeyHistory.length) options.startKeyHistory = this.state.historyStartKeyHistory.slice();
|
|
5515
5567
|
if (!fetchMore) options.deferBg = true;
|
|
5516
5568
|
var fetchHistory = function() {
|
|
5517
|
-
return getSplitChatHistory({
|
|
5569
|
+
return getSplitChatHistory({
|
|
5570
|
+
service: projectId,
|
|
5571
|
+
owner,
|
|
5572
|
+
platform,
|
|
5573
|
+
userId: id.userId,
|
|
5574
|
+
// An anonymous visitor's history is scoped server side by
|
|
5575
|
+
// ip + "(" + user_agent + ")", which two devices behind one NAT
|
|
5576
|
+
// share. Read this device's own queue instead. See
|
|
5577
|
+
// scopeSurfaceToQueue.
|
|
5578
|
+
scopeSurfaceToQueue: !!id.anonymous
|
|
5579
|
+
}, options);
|
|
5518
5580
|
};
|
|
5519
5581
|
return Promise.resolve().then(fetchHistory).catch(function(err) {
|
|
5520
5582
|
if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
|
|
@@ -5536,6 +5598,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5536
5598
|
var mapped = mapHistoryListToMessages(list, platform, {
|
|
5537
5599
|
clearedAt: self.host.getClearedAt(),
|
|
5538
5600
|
projectId: id.projectId,
|
|
5601
|
+
// So the `_ownerKey` stamped on server history matches loadKey and
|
|
5602
|
+
// the cache key. Without it every mapped bubble carries a
|
|
5603
|
+
// two-segment stamp that no comparison can ever match.
|
|
5604
|
+
userId: id.userId,
|
|
5539
5605
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
5540
5606
|
}).messages;
|
|
5541
5607
|
self.applyHydratedBodies(mapped);
|
|
@@ -5744,6 +5810,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5744
5810
|
var m2 = mapHistoryListToMessages(sorted, platform, {
|
|
5745
5811
|
clearedAt: self.host.getClearedAt(),
|
|
5746
5812
|
projectId: id.projectId,
|
|
5813
|
+
userId: id.userId,
|
|
5747
5814
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
5748
5815
|
}).messages;
|
|
5749
5816
|
self.applyHydratedBodies(m2);
|
|
@@ -5977,6 +6044,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5977
6044
|
})).catch(function() {
|
|
5978
6045
|
});
|
|
5979
6046
|
});
|
|
6047
|
+
var accessGroup;
|
|
6048
|
+
preIndex = preIndex.then(function() {
|
|
6049
|
+
if (alreadyIndexing) return;
|
|
6050
|
+
if (typeof self.host.uploadAccessGroup !== "function") return;
|
|
6051
|
+
return Promise.resolve(self.host.uploadAccessGroup(member.storagePath)).then(function(g) {
|
|
6052
|
+
accessGroup = g || void 0;
|
|
6053
|
+
}).catch(function() {
|
|
6054
|
+
});
|
|
6055
|
+
});
|
|
5980
6056
|
return preIndex.then(function() {
|
|
5981
6057
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
5982
6058
|
}).then(function(parsedContent) {
|
|
@@ -5995,7 +6071,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5995
6071
|
storagePath: member.storagePath,
|
|
5996
6072
|
mime: mime || void 0,
|
|
5997
6073
|
size: member.file.size,
|
|
5998
|
-
url
|
|
6074
|
+
url,
|
|
6075
|
+
accessGroup
|
|
5999
6076
|
},
|
|
6000
6077
|
parsedContent: parsedContent || void 0
|
|
6001
6078
|
}).then(function(ack) {
|
|
@@ -6526,7 +6603,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6526
6603
|
(function() {
|
|
6527
6604
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
6528
6605
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
6529
|
-
var BQ_VERSION = "1.9.
|
|
6606
|
+
var BQ_VERSION = "1.9.5" ;
|
|
6530
6607
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
6531
6608
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
6532
6609
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -6542,7 +6619,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6542
6619
|
// sessionStorage
|
|
6543
6620
|
googleRedirect: "bq_embed:google_redirect",
|
|
6544
6621
|
// sessionStorage
|
|
6545
|
-
clearHorizon: "bq_embed:clearedAt"
|
|
6622
|
+
clearHorizon: "bq_embed:clearedAt",
|
|
6623
|
+
anonId: "bq_embed:anon_id"
|
|
6624
|
+
// per-project anonymous device id
|
|
6546
6625
|
};
|
|
6547
6626
|
function h(tag, attrs) {
|
|
6548
6627
|
var el = document.createElement(tag);
|
|
@@ -6697,6 +6776,40 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6697
6776
|
function skey(base) {
|
|
6698
6777
|
return base + ":" + (S.projectId || "default");
|
|
6699
6778
|
}
|
|
6779
|
+
function anonymousAllowed() {
|
|
6780
|
+
if (S.opts && typeof S.opts.allowAnonymous === "boolean") return S.opts.allowAnonymous;
|
|
6781
|
+
var conf = S.service && S.service.conf || null;
|
|
6782
|
+
if (!conf || typeof conf.prevent_anonymous === "undefined") return false;
|
|
6783
|
+
return !conf.prevent_anonymous;
|
|
6784
|
+
}
|
|
6785
|
+
function isAnonymousSession() {
|
|
6786
|
+
return !S.user && anonymousAllowed();
|
|
6787
|
+
}
|
|
6788
|
+
function randomId() {
|
|
6789
|
+
try {
|
|
6790
|
+
var buf = new Uint8Array(16);
|
|
6791
|
+
(window.crypto || window.msCrypto).getRandomValues(buf);
|
|
6792
|
+
var out = "";
|
|
6793
|
+
for (var i = 0; i < buf.length; i++) out += ("0" + buf[i].toString(16)).slice(-2);
|
|
6794
|
+
return out;
|
|
6795
|
+
} catch (e) {
|
|
6796
|
+
return "x" + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
var _anonIdMemo = null;
|
|
6800
|
+
function anonDeviceId() {
|
|
6801
|
+
if (_anonIdMemo) return _anonIdMemo;
|
|
6802
|
+
var key = skey(SK.anonId);
|
|
6803
|
+
var stored = lsGet(key);
|
|
6804
|
+
if (stored) {
|
|
6805
|
+
_anonIdMemo = stored;
|
|
6806
|
+
return stored;
|
|
6807
|
+
}
|
|
6808
|
+
var minted = "anon_" + randomId();
|
|
6809
|
+
lsSet(key, minted);
|
|
6810
|
+
_anonIdMemo = lsGet(key) || minted;
|
|
6811
|
+
return _anonIdMemo;
|
|
6812
|
+
}
|
|
6700
6813
|
function loadTheme() {
|
|
6701
6814
|
var stored = lsGet(SK.theme);
|
|
6702
6815
|
if (stored === "dark" || stored === "light") return stored;
|
|
@@ -7225,6 +7338,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7225
7338
|
text: "Sign up \u2192"
|
|
7226
7339
|
}));
|
|
7227
7340
|
}
|
|
7341
|
+
var canReturnToChat = !S.user && anonymousAllowed();
|
|
7228
7342
|
var form = h(
|
|
7229
7343
|
"form",
|
|
7230
7344
|
{ class: "bq-form", onsubmit: submit },
|
|
@@ -7234,7 +7348,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7234
7348
|
errorBox,
|
|
7235
7349
|
h("div", { class: "bq-form-bottom" }, submitBtn)
|
|
7236
7350
|
);
|
|
7237
|
-
var children =
|
|
7351
|
+
var children = [];
|
|
7352
|
+
if (canReturnToChat) {
|
|
7353
|
+
children.push(h(
|
|
7354
|
+
"div",
|
|
7355
|
+
{ class: "bq-settings-top" },
|
|
7356
|
+
h("button", {
|
|
7357
|
+
class: "bq-link",
|
|
7358
|
+
type: "button",
|
|
7359
|
+
onclick: function() {
|
|
7360
|
+
enterAfterLogin();
|
|
7361
|
+
},
|
|
7362
|
+
text: "\u2190 Back to chat"
|
|
7363
|
+
})
|
|
7364
|
+
));
|
|
7365
|
+
}
|
|
7366
|
+
children = children.concat(authHeader("Login")).concat([form]);
|
|
7238
7367
|
if (googleEnabled()) {
|
|
7239
7368
|
children.push(
|
|
7240
7369
|
h(
|
|
@@ -8185,7 +8314,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8185
8314
|
return void 0;
|
|
8186
8315
|
})(),
|
|
8187
8316
|
owner: S.owner,
|
|
8188
|
-
|
|
8317
|
+
// The chat identity, which the engine turns into the request queue
|
|
8318
|
+
// name. An anonymous visitor gets their DEVICE id rather than the
|
|
8319
|
+
// project id: the old fallback gave every anonymous visitor of a
|
|
8320
|
+
// project the same queue, so they would have shared one transcript
|
|
8321
|
+
// and head-of-line-blocked each other's turns on a single FIFO.
|
|
8322
|
+
userId: S.user && S.user.user_id || (isAnonymousSession() ? anonDeviceId() : S.projectId),
|
|
8323
|
+
// Sends the turn's MCP tools to the project-scoped, credential-free
|
|
8324
|
+
// endpoint instead of the root one with an empty bearer.
|
|
8325
|
+
anonymous: isAnonymousSession(),
|
|
8189
8326
|
platform: S.aiPlatform,
|
|
8190
8327
|
model: S.aiModel || void 0,
|
|
8191
8328
|
serviceName: S.serviceName,
|
|
@@ -8247,7 +8384,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8247
8384
|
return deleteFileIndexRecordDb(path);
|
|
8248
8385
|
},
|
|
8249
8386
|
ensureFileIndexRecord: function(path, meta) {
|
|
8250
|
-
return
|
|
8387
|
+
return resolveUploadAccessGroup(path).then(function(g) {
|
|
8388
|
+
return ensureFileIndexRecordDb(path, meta, g);
|
|
8389
|
+
});
|
|
8390
|
+
},
|
|
8391
|
+
uploadAccessGroup: function(path) {
|
|
8392
|
+
return resolveUploadAccessGroup(path);
|
|
8251
8393
|
},
|
|
8252
8394
|
storagePathFor: function(relPath) {
|
|
8253
8395
|
return attachmentStoragePath(relPath);
|
|
@@ -8259,7 +8401,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8259
8401
|
return promptOverwrite(filename);
|
|
8260
8402
|
},
|
|
8261
8403
|
resetOverwriteBatch: function() {
|
|
8262
|
-
|
|
8404
|
+
resetOverwriteBatch();
|
|
8405
|
+
resetAccessGroupBatch();
|
|
8263
8406
|
},
|
|
8264
8407
|
renderAttachmentChips: function() {
|
|
8265
8408
|
renderAttachmentChips();
|
|
@@ -8370,7 +8513,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8370
8513
|
// paints), so the model is told what it opened with. Same call as
|
|
8371
8514
|
// buildGreetingEl, so the two can never disagree.
|
|
8372
8515
|
greeting: greetingParts().text,
|
|
8373
|
-
canUpload: !uploadsFrozenForUser()
|
|
8516
|
+
canUpload: !uploadsFrozenForUser(),
|
|
8517
|
+
// Where THIS project's indexer writes. The MCP's auto-fill assumes
|
|
8518
|
+
// "authorized"; on a project set to public or private that would
|
|
8519
|
+
// search the wrong group and answer "nothing found".
|
|
8520
|
+
indexAccessGroup: projectUploadAccessGroup()});
|
|
8374
8521
|
}
|
|
8375
8522
|
function refreshSkapiSession() {
|
|
8376
8523
|
return S.skapi.getProfile({ refreshToken: true }).then(function() {
|
|
@@ -8813,12 +8960,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8813
8960
|
}
|
|
8814
8961
|
return createChain();
|
|
8815
8962
|
}
|
|
8816
|
-
function ensureFileIndexRecordDb(storagePath, meta) {
|
|
8963
|
+
function ensureFileIndexRecordDb(storagePath, meta, accessGroup) {
|
|
8817
8964
|
if (!storagePath || !S.skapi || typeof S.skapi.postRecord !== "function") return Promise.resolve();
|
|
8818
8965
|
return Promise.resolve(S.skapi.postRecord(null, {
|
|
8819
8966
|
service: S.projectId,
|
|
8820
8967
|
unique_id: "src::" + storagePath,
|
|
8821
|
-
table: { name: "file_summaries", access_group:
|
|
8968
|
+
table: { name: "file_summaries", access_group: normalizeUploadAccessGroup(accessGroup) },
|
|
8822
8969
|
// Deleting the file record must cascade to every record referencing it.
|
|
8823
8970
|
source: { can_remove_referencing_records: true },
|
|
8824
8971
|
data: {
|
|
@@ -9517,7 +9664,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9517
9664
|
}
|
|
9518
9665
|
function getClearHistoryStorageKey() {
|
|
9519
9666
|
if (!S.projectId || S.aiPlatform === "none") return "";
|
|
9520
|
-
|
|
9667
|
+
var key = SK.clearHorizon + ":" + S.projectId + "#" + S.aiPlatform;
|
|
9668
|
+
if (isAnonymousSession()) key += "#" + anonDeviceId();
|
|
9669
|
+
return key;
|
|
9521
9670
|
}
|
|
9522
9671
|
function getClearedAt() {
|
|
9523
9672
|
var key = getClearHistoryStorageKey();
|
|
@@ -9881,7 +10030,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9881
10030
|
var liveIndex = session.getLiveIndexState();
|
|
9882
10031
|
var fresh = markerSweep.svc === S.projectId;
|
|
9883
10032
|
var stubs = void 0;
|
|
9884
|
-
if (fresh) {
|
|
10033
|
+
if (fresh && !isAnonymousSession()) {
|
|
9885
10034
|
stubs = {};
|
|
9886
10035
|
var myId = S.user && S.user.user_id || "";
|
|
9887
10036
|
for (var rp in markerSweep.runs) {
|
|
@@ -10392,6 +10541,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10392
10541
|
}
|
|
10393
10542
|
});
|
|
10394
10543
|
CS.settingsBtnEl = settingsBtn;
|
|
10544
|
+
var headerRight = isAnonymousSession() ? h("button", {
|
|
10545
|
+
class: "bq-link",
|
|
10546
|
+
type: "button",
|
|
10547
|
+
title: "Login",
|
|
10548
|
+
onclick: function() {
|
|
10549
|
+
renderLogin();
|
|
10550
|
+
},
|
|
10551
|
+
text: "Login"
|
|
10552
|
+
}) : settingsBtn;
|
|
10553
|
+
if (isAnonymousSession()) CS.settingsBtnEl = null;
|
|
10395
10554
|
var header = h(
|
|
10396
10555
|
"div",
|
|
10397
10556
|
{ class: "bq-section-title" },
|
|
@@ -10399,7 +10558,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10399
10558
|
"div",
|
|
10400
10559
|
{ class: "bq-title-row" },
|
|
10401
10560
|
brandTitleEl(),
|
|
10402
|
-
h("div", { class: "bq-title-right" },
|
|
10561
|
+
h("div", { class: "bq-title-right" }, headerRight)
|
|
10403
10562
|
)
|
|
10404
10563
|
);
|
|
10405
10564
|
var chatArea;
|
|
@@ -10536,6 +10695,128 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10536
10695
|
overwriteState.resolver = null;
|
|
10537
10696
|
if (r) r(choice);
|
|
10538
10697
|
}
|
|
10698
|
+
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
10699
|
+
var UPLOAD_ACCESS_LABELS = {
|
|
10700
|
+
public: "Public",
|
|
10701
|
+
authorized: "Signed in users",
|
|
10702
|
+
private: "Only me"
|
|
10703
|
+
};
|
|
10704
|
+
var UPLOAD_ACCESS_HINTS = {
|
|
10705
|
+
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
10706
|
+
authorized: "Only users signed in to this project can ask about this file.",
|
|
10707
|
+
private: "Only you can ask about this file."
|
|
10708
|
+
};
|
|
10709
|
+
function normalizeUploadAccessGroup(v) {
|
|
10710
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(v) === -1 ? "authorized" : v;
|
|
10711
|
+
}
|
|
10712
|
+
function projectAccessSetting() {
|
|
10713
|
+
var conf = S.service && S.service.conf || {};
|
|
10714
|
+
var v = conf.default_access_group;
|
|
10715
|
+
if (v === "ask") return "ask";
|
|
10716
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(v) === -1 ? null : v;
|
|
10717
|
+
}
|
|
10718
|
+
function projectUploadAccessGroup() {
|
|
10719
|
+
var v = projectAccessSetting();
|
|
10720
|
+
return v && v !== "ask" ? v : "authorized";
|
|
10721
|
+
}
|
|
10722
|
+
function projectAsksUploadAccess() {
|
|
10723
|
+
return projectAccessSetting() === "ask";
|
|
10724
|
+
}
|
|
10725
|
+
var accessGroupState = { resolver: null, sticky: null, handle: null, applyToAll: false, choice: "authorized", perPath: {} };
|
|
10726
|
+
function resetAccessGroupBatch() {
|
|
10727
|
+
accessGroupState.sticky = null;
|
|
10728
|
+
accessGroupState.applyToAll = false;
|
|
10729
|
+
accessGroupState.perPath = {};
|
|
10730
|
+
}
|
|
10731
|
+
function chooseAccessGroup(choice) {
|
|
10732
|
+
var picked = normalizeUploadAccessGroup(choice);
|
|
10733
|
+
if (accessGroupState.applyToAll) accessGroupState.sticky = picked;
|
|
10734
|
+
if (accessGroupState.handle) {
|
|
10735
|
+
accessGroupState.handle.close();
|
|
10736
|
+
accessGroupState.handle = null;
|
|
10737
|
+
}
|
|
10738
|
+
var r = accessGroupState.resolver;
|
|
10739
|
+
accessGroupState.resolver = null;
|
|
10740
|
+
if (r) r(picked);
|
|
10741
|
+
}
|
|
10742
|
+
var accessGroupChain = Promise.resolve();
|
|
10743
|
+
function resolveUploadAccessGroup(storagePath) {
|
|
10744
|
+
if (!projectAsksUploadAccess()) return Promise.resolve(projectUploadAccessGroup());
|
|
10745
|
+
var fallback = projectUploadAccessGroup();
|
|
10746
|
+
if (accessGroupState.sticky) return Promise.resolve(accessGroupState.sticky);
|
|
10747
|
+
var pathKey = String(storagePath || "");
|
|
10748
|
+
if (pathKey && accessGroupState.perPath[pathKey]) {
|
|
10749
|
+
return Promise.resolve(accessGroupState.perPath[pathKey]);
|
|
10750
|
+
}
|
|
10751
|
+
var run = accessGroupChain.then(function() {
|
|
10752
|
+
if (accessGroupState.sticky) return accessGroupState.sticky;
|
|
10753
|
+
if (pathKey && accessGroupState.perPath[pathKey]) {
|
|
10754
|
+
return accessGroupState.perPath[pathKey];
|
|
10755
|
+
}
|
|
10756
|
+
accessGroupState.applyToAll = false;
|
|
10757
|
+
accessGroupState.choice = fallback;
|
|
10758
|
+
var filename = String(storagePath || "").split("/").pop() || "this file";
|
|
10759
|
+
return new Promise(function(resolve) {
|
|
10760
|
+
accessGroupState.resolver = resolve;
|
|
10761
|
+
accessGroupState.handle = openModal(function() {
|
|
10762
|
+
var list = h("div", { class: "bq-access-options" });
|
|
10763
|
+
UPLOAD_ACCESS_GROUPS.forEach(function(g) {
|
|
10764
|
+
var input = h("input", { type: "radio", name: "bq-access-group", value: g });
|
|
10765
|
+
input.checked = g === accessGroupState.choice;
|
|
10766
|
+
input.addEventListener("change", function() {
|
|
10767
|
+
if (input.checked) accessGroupState.choice = g;
|
|
10768
|
+
});
|
|
10769
|
+
list.appendChild(h(
|
|
10770
|
+
"label",
|
|
10771
|
+
{ class: "bq-access-option" },
|
|
10772
|
+
input,
|
|
10773
|
+
h("span", { class: "bq-access-option-label", text: UPLOAD_ACCESS_LABELS[g] }),
|
|
10774
|
+
h("span", { class: "bq-access-option-hint", text: UPLOAD_ACCESS_HINTS[g] })
|
|
10775
|
+
));
|
|
10776
|
+
});
|
|
10777
|
+
var applyCb = h("input", { type: "checkbox" });
|
|
10778
|
+
applyCb.addEventListener("change", function() {
|
|
10779
|
+
accessGroupState.applyToAll = !!applyCb.checked;
|
|
10780
|
+
});
|
|
10781
|
+
var applyLabel = h(
|
|
10782
|
+
"label",
|
|
10783
|
+
{ class: "bq-overwrite-applyall" },
|
|
10784
|
+
applyCb,
|
|
10785
|
+
h("span", { text: "Apply to all remaining files" })
|
|
10786
|
+
);
|
|
10787
|
+
return h(
|
|
10788
|
+
"div",
|
|
10789
|
+
{ class: "bq-modal" },
|
|
10790
|
+
h("div", { class: "bq-modal-delete-header" }, h("span", { text: "Who can read this file?" })),
|
|
10791
|
+
h(
|
|
10792
|
+
"p",
|
|
10793
|
+
{ class: "bq-modal-desc" },
|
|
10794
|
+
"Choose who can ask questions about \u201C" + filename + "\u201D once it is indexed."
|
|
10795
|
+
),
|
|
10796
|
+
list,
|
|
10797
|
+
applyLabel,
|
|
10798
|
+
h(
|
|
10799
|
+
"div",
|
|
10800
|
+
{ class: "bq-modal-btns" },
|
|
10801
|
+
h("button", { class: "btn", type: "button", onclick: function() {
|
|
10802
|
+
chooseAccessGroup(accessGroupState.choice);
|
|
10803
|
+
} }, "Upload")
|
|
10804
|
+
)
|
|
10805
|
+
);
|
|
10806
|
+
}, { dismissible: false });
|
|
10807
|
+
});
|
|
10808
|
+
});
|
|
10809
|
+
accessGroupChain = run.catch(function() {
|
|
10810
|
+
return void 0;
|
|
10811
|
+
});
|
|
10812
|
+
return run.then(function(picked) {
|
|
10813
|
+
var g = normalizeUploadAccessGroup(picked);
|
|
10814
|
+
if (pathKey) accessGroupState.perPath[pathKey] = g;
|
|
10815
|
+
return g;
|
|
10816
|
+
}).catch(function() {
|
|
10817
|
+
return fallback;
|
|
10818
|
+
});
|
|
10819
|
+
}
|
|
10539
10820
|
function promptOverwrite(filename) {
|
|
10540
10821
|
if (overwriteState.sticky) return Promise.resolve(overwriteState.sticky);
|
|
10541
10822
|
overwriteState.applyToAll = false;
|
|
@@ -10647,6 +10928,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10647
10928
|
}).catch(function() {
|
|
10648
10929
|
}).then(function() {
|
|
10649
10930
|
S.user = null;
|
|
10931
|
+
if (anonymousAllowed()) return enterAfterLogin();
|
|
10650
10932
|
renderLogin();
|
|
10651
10933
|
});
|
|
10652
10934
|
}
|
|
@@ -10660,7 +10942,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10660
10942
|
}).then(function() {
|
|
10661
10943
|
return loadServiceInfo();
|
|
10662
10944
|
}).then(function(conn) {
|
|
10663
|
-
S.service = conn;
|
|
10945
|
+
if (conn) S.service = conn;
|
|
10664
10946
|
applyAgentConfig();
|
|
10665
10947
|
}).then(function() {
|
|
10666
10948
|
renderChat();
|
|
@@ -10720,6 +11002,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10720
11002
|
return getProfile().then(function(user) {
|
|
10721
11003
|
S.user = user;
|
|
10722
11004
|
if (!user) {
|
|
11005
|
+
if (anonymousAllowed()) return enterAfterLogin();
|
|
10723
11006
|
renderLogin();
|
|
10724
11007
|
return;
|
|
10725
11008
|
}
|
|
@@ -10758,8 +11041,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10758
11041
|
// defaults to current host page
|
|
10759
11042
|
hostDomain: null,
|
|
10760
11043
|
// db-CDN host; null → skapi.app (dev) / skapi.com (prod)
|
|
10761
|
-
attachmentParsers: null
|
|
11044
|
+
attachmentParsers: null,
|
|
10762
11045
|
// client-side attachment parsers, e.g. [createHwpParser()]
|
|
11046
|
+
// Open the chat with no login for visitors without an account.
|
|
11047
|
+
// null → follow the project's own "Allow anonymous users" setting
|
|
11048
|
+
// (getConnectionInfo().conf.prevent_anonymous); true/false pins it.
|
|
11049
|
+
allowAnonymous: null,
|
|
11050
|
+
// Server-driven windowed indexing; read at configureChatEngine time.
|
|
11051
|
+
// Listed here so the defaults object is the full opt surface.
|
|
11052
|
+
windowedIndexing: true
|
|
10763
11053
|
}, opts || {});
|
|
10764
11054
|
S.mountEl = mountEl;
|
|
10765
11055
|
clear(mountEl);
|