Add chunked timeline routing
This commit is contained in:
+108
-12
@@ -400,6 +400,7 @@ let lastOriginalText = '';
|
||||
let lastRedactPayload = null;
|
||||
let lastRunEngine = null;
|
||||
let lastToolPayload = null;
|
||||
let pendingTimelineQuote = null;
|
||||
|
||||
const VOCAB_PRESETS = {
|
||||
barnerett: 'Barnevernet, Fylkesnemnda, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, sakkyndig, advokat, prosessfullmektig, dommer, vitne, tolk, bistandsadvokat, fosterforeldre, fosterhjem, akuttvedtak, statsforvalter, Bufetat, saksbehandler, rettslig medhold, begjæring, samtykke, tilsynsfører',
|
||||
@@ -764,10 +765,60 @@ function timelineEngineLabel(engine) {
|
||||
}
|
||||
|
||||
function timelineClientRoute(engine, charCount) {
|
||||
let effective = engine;
|
||||
if (charCount > 55000) effective = 'azure_full';
|
||||
else if (charCount > 25000 && effective === 'nova_lite') effective = 'azure_mini';
|
||||
return { effective, upgraded: effective !== engine };
|
||||
return timelineClientQuote(engine, charCount);
|
||||
}
|
||||
|
||||
function timelineClientQuote(engine, charCount) {
|
||||
const valid = ['nova_lite', 'azure_mini', 'azure_full'];
|
||||
const requested = valid.includes(engine) ? engine : 'azure_mini';
|
||||
const singleLimits = { nova_lite: 25000, azure_mini: 55000, azure_full: 128000 };
|
||||
const maxLimits = { nova_lite: 100000, azure_mini: 300000, azure_full: 600000 };
|
||||
const chunkSizes = { nova_lite: 10000, azure_mini: 16000, azure_full: 30000 };
|
||||
const ranks = { nova_lite: 1, azure_mini: 2, azure_full: 3 };
|
||||
const baseCredits = requested === 'azure_full' ? 2 : 1;
|
||||
let effective = requested;
|
||||
|
||||
if (charCount > 600000) {
|
||||
return {
|
||||
error: true,
|
||||
message: `This timeline input is ${charCount.toLocaleString()} characters. Split the file or use fewer selected documents; the current maximum is 600,000 characters.`,
|
||||
};
|
||||
}
|
||||
if (charCount > maxLimits[effective]) {
|
||||
effective = charCount <= maxLimits.azure_mini ? 'azure_mini' : 'azure_full';
|
||||
}
|
||||
if (charCount > maxLimits[effective]) effective = 'azure_full';
|
||||
|
||||
let credits = 1;
|
||||
if (effective === 'nova_lite') {
|
||||
credits = charCount <= singleLimits.nova_lite ? 1 : 2;
|
||||
} else if (effective === 'azure_mini') {
|
||||
credits = charCount <= singleLimits.azure_mini ? 1 : (charCount <= 180000 ? 2 : 3);
|
||||
} else {
|
||||
credits = charCount <= singleLimits.azure_full ? 2 : (charCount <= 350000 ? 4 : 6);
|
||||
}
|
||||
|
||||
const chunked = charCount > singleLimits[effective];
|
||||
return {
|
||||
requested,
|
||||
effective,
|
||||
upgraded: ranks[effective] > ranks[requested],
|
||||
charCount,
|
||||
credits,
|
||||
baseCredits,
|
||||
chunked,
|
||||
chunkCount: chunked ? Math.ceil(charCount / chunkSizes[effective]) : 1,
|
||||
requiresConfirmation: credits > baseCredits || ranks[effective] > ranks[requested],
|
||||
};
|
||||
}
|
||||
|
||||
function timelineQuoteMessage(quote) {
|
||||
return [
|
||||
`Timeline will use ${timelineEngineLabel(quote.effective)} for ${Number(quote.charCount || 0).toLocaleString()} characters.`,
|
||||
quote.chunked ? `It will process about ${quote.chunkCount} chunks.` : 'It can run in a single pass.',
|
||||
`Cost: ${quote.credits} credit${quote.credits === 1 ? '' : 's'}.`,
|
||||
'Continue?'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function currentTimelineFocus() {
|
||||
@@ -1122,15 +1173,36 @@ async function runTool(event) {
|
||||
let timelineRouteNotice = '';
|
||||
if (state.activeTool === 'timeline') {
|
||||
payload.engine = currentTimelineEngine();
|
||||
const clientRoute = timelineClientRoute(payload.engine, text.length);
|
||||
const clientRoute = timelineClientQuote(payload.engine, text.length);
|
||||
if (clientRoute.error) {
|
||||
els.status.textContent = clientRoute.message;
|
||||
return;
|
||||
}
|
||||
const pendingQuoteApplies = pendingTimelineQuote
|
||||
&& pendingTimelineQuote.text === text
|
||||
&& pendingTimelineQuote.requested === payload.engine;
|
||||
if (pendingQuoteApplies) {
|
||||
payload.accepted_timeline_quote = true;
|
||||
payload.accepted_credits = pendingTimelineQuote.credits;
|
||||
payload.accepted_effective_engine = pendingTimelineQuote.effective;
|
||||
pendingTimelineQuote = null;
|
||||
} else if (clientRoute.requiresConfirmation) {
|
||||
if (!window.confirm(timelineQuoteMessage(clientRoute))) {
|
||||
els.status.textContent = 'Timeline run cancelled before any credits were charged.';
|
||||
return;
|
||||
}
|
||||
payload.accepted_timeline_quote = true;
|
||||
payload.accepted_credits = clientRoute.credits;
|
||||
payload.accepted_effective_engine = clientRoute.effective;
|
||||
}
|
||||
payload.focus = currentTimelineFocus();
|
||||
payload.confidence_filter = currentConfidenceFilter();
|
||||
payload.include_relative = currentIncludeRelative();
|
||||
payload.include_background = currentIncludeBackground();
|
||||
payload.user_notes = (document.getElementById('timelineNotes')?.value || '').trim();
|
||||
payload.use_my_case = (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false;
|
||||
timelineRouteNotice = clientRoute.upgraded
|
||||
? `This input is ${text.length.toLocaleString()} characters, so Timeline will use ${timelineEngineLabel(clientRoute.effective)} for reliability.`
|
||||
timelineRouteNotice = clientRoute.upgraded || clientRoute.chunked
|
||||
? `This input is ${text.length.toLocaleString()} characters, so Timeline will use ${timelineEngineLabel(clientRoute.effective)}${clientRoute.chunked ? ` across about ${clientRoute.chunkCount} chunks` : ''}.`
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -1157,7 +1229,30 @@ async function runTool(event) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
if (!resp.ok) {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
const quote = errData.timeline_quote;
|
||||
if (errData.error?.code === 'timeline_quote_required' && quote) {
|
||||
const confirmQuote = {
|
||||
effective: quote.effective_engine,
|
||||
charCount: quote.input_char_count,
|
||||
credits: quote.credits || quote.estimated_credits,
|
||||
chunked: Boolean(quote.chunked_timeline),
|
||||
chunkCount: quote.timeline_chunk_count || 1,
|
||||
};
|
||||
if (window.confirm(timelineQuoteMessage(confirmQuote))) {
|
||||
pendingTimelineQuote = {
|
||||
text,
|
||||
requested: payload.engine,
|
||||
effective: confirmQuote.effective,
|
||||
credits: Number(confirmQuote.credits || 0),
|
||||
};
|
||||
return runTool(event);
|
||||
}
|
||||
throw new Error('Timeline run cancelled before any credits were charged.');
|
||||
}
|
||||
throw new Error(errData.error?.message || `HTTP ${resp.status}`);
|
||||
}
|
||||
const reader = resp.body.getReader();
|
||||
const dec = new TextDecoder();
|
||||
let buf = '', event = '';
|
||||
@@ -1194,8 +1289,8 @@ async function runTool(event) {
|
||||
renderResults(data);
|
||||
renderTrace(data.trace || []);
|
||||
const routeMeta = data.trace_metadata || {};
|
||||
const serverRouteNotice = state.activeTool === 'timeline' && routeMeta.auto_upgraded_engine
|
||||
? ` Used ${timelineEngineLabel(routeMeta.effective_engine)} for ${Number(routeMeta.input_char_count || 0).toLocaleString()} characters.`
|
||||
const serverRouteNotice = state.activeTool === 'timeline' && (routeMeta.auto_upgraded_engine || routeMeta.chunked_timeline || routeMeta.credits_charged)
|
||||
? ` Used ${timelineEngineLabel(routeMeta.effective_engine)} for ${Number(routeMeta.input_char_count || 0).toLocaleString()} characters${routeMeta.chunked_timeline ? ` across ${routeMeta.timeline_chunk_count || 1} chunks` : ''}; charged ${routeMeta.credits_charged || routeMeta.estimated_credits || 1} credit(s).`
|
||||
: '';
|
||||
els.status.textContent = `Done in ${data.latency_ms || 0} ms.${serverRouteNotice}`;
|
||||
if (['ask', 'redact', 'timeline'].includes(state.activeTool)) {
|
||||
@@ -1299,6 +1394,7 @@ async function handleFiles(fileList) {
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('tool', state.activeTool);
|
||||
|
||||
const resp = await fetch('api/extract.php', {
|
||||
method: 'POST',
|
||||
@@ -1318,7 +1414,7 @@ async function handleFiles(fileList) {
|
||||
|
||||
const combined = parts[0].text;
|
||||
|
||||
const MAX_COMBINED = 128000;
|
||||
const MAX_COMBINED = state.activeTool === 'timeline' ? 600000 : 128000;
|
||||
const combinedTruncated = combined.length > MAX_COMBINED;
|
||||
els.input.value = combinedTruncated ? combined.slice(0, MAX_COMBINED) : combined;
|
||||
|
||||
@@ -1328,7 +1424,7 @@ async function handleFiles(fileList) {
|
||||
els.uploadPrompt.classList.add('is-hidden');
|
||||
els.uploadFileInfo.classList.remove('is-hidden');
|
||||
|
||||
const truncNote = (anyTruncated || combinedTruncated) ? ' — truncated to 128 000 char limit' : '';
|
||||
const truncNote = (anyTruncated || combinedTruncated) ? ` - truncated to ${MAX_COMBINED.toLocaleString()} char limit` : '';
|
||||
els.status.textContent = `Extracted ${totalChars.toLocaleString()} chars from ${parts[0].filename}${truncNote}.`;
|
||||
} catch (err) {
|
||||
els.status.textContent = err.message;
|
||||
|
||||
Reference in New Issue
Block a user