llm_meta_widget 0.5.0 → 0.6.0
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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f019c93684d03e4f65ead3c8427b369af41caf2b9f4b0637f60aa2014239f080
|
|
4
|
+
data.tar.gz: 28bf06bc291b4bc0f745b85ae7f067f9e9c3f8adfd4902d747a4496d1b2e34da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50a79ea50cd118b4cafb1a26ff5af6713fd6b98dd1374ad4a2b2fa10fff24677fe417cee3b4cf4d900e121b3b4be3c013fcbc1aa95d4183f714e629a100432c8
|
|
7
|
+
data.tar.gz: 1bbed09f23f1c5c7f9b08500db77d2dd1b7ba97e1b564405b40f8cadd9fb1bac920a4d612e187119d08cd90db51301b96dc8d3d3ad22e0196adc04b5ee1621c7
|
data/README.md
CHANGED
|
@@ -232,6 +232,23 @@ To lock the widget to the fixed `model:` prop and disable Class-1 tools entirely
|
|
|
232
232
|
| `models:` | `nil` | Model-name allowlist; `nil` = all anon-available |
|
|
233
233
|
| `hub_tools:` | `nil` | MCP-server-name allowlist; `nil` = all anon-public |
|
|
234
234
|
|
|
235
|
+
## Surviving navigation
|
|
236
|
+
|
|
237
|
+
A page action that navigates — submitting a form, following a link — no longer
|
|
238
|
+
loses the conversation. The transcript, the record of which tools ran, and the
|
|
239
|
+
model's own context are kept in `sessionStorage`: per tab, gone when the tab
|
|
240
|
+
closes, never sent anywhere.
|
|
241
|
+
|
|
242
|
+
The key is the page's **path**, not its full URL, so a form submit that returns
|
|
243
|
+
to the same page with a new query string keeps the thread, while moving to a
|
|
244
|
+
different page starts a fresh one. Nothing to configure.
|
|
245
|
+
|
|
246
|
+
Two consequences worth knowing. An action of yours that navigates is now safe
|
|
247
|
+
to declare — before this, offering one meant offering to wipe the visitor's
|
|
248
|
+
chat. And if the navigation lands on an error page that does not render the
|
|
249
|
+
widget, the conversation is still in storage but there is no panel to show it
|
|
250
|
+
until the visitor returns to a page that has one.
|
|
251
|
+
|
|
235
252
|
## Choosing what answers the chat, and whose tools to offer
|
|
236
253
|
|
|
237
254
|
Two independent settings, because they are two jobs. Neither implies the
|
|
@@ -1185,3 +1185,74 @@ export async function fetchOllamaModels({ baseUrl, signal }) {
|
|
|
1185
1185
|
return []
|
|
1186
1186
|
}
|
|
1187
1187
|
}
|
|
1188
|
+
|
|
1189
|
+
// ---- surviving navigation ------------------------------------------------
|
|
1190
|
+
//
|
|
1191
|
+
// A page action that navigates — submitting a form, following a link — used to
|
|
1192
|
+
// destroy the conversation, the record of what ran and the scroll position.
|
|
1193
|
+
// That made such actions unsafe to offer at all: PubDictionaries had to stop
|
|
1194
|
+
// declaring its submit action rather than let the assistant wipe the chat.
|
|
1195
|
+
//
|
|
1196
|
+
// The transcript is kept in sessionStorage: per tab, gone when the tab closes,
|
|
1197
|
+
// invisible to other visitors. Keyed by pathname rather than full URL, because
|
|
1198
|
+
// submitting a form usually returns to the same page with different query
|
|
1199
|
+
// parameters and that is exactly the case worth surviving.
|
|
1200
|
+
|
|
1201
|
+
export const CONVERSATION_FORMAT = 1
|
|
1202
|
+
|
|
1203
|
+
export function packConversation({ turns = [], open = false, model = null,
|
|
1204
|
+
maxTurns = 40, maxBytes = 200000 } = {}) {
|
|
1205
|
+
// Oldest turns go first when trimming: the recent ones carry the thread.
|
|
1206
|
+
let kept = turns.slice(-maxTurns)
|
|
1207
|
+
let payload = { v: CONVERSATION_FORMAT, savedAt: Date.now(), open, model, turns: kept }
|
|
1208
|
+
let json = JSON.stringify(payload)
|
|
1209
|
+
|
|
1210
|
+
while (json.length > maxBytes && kept.length > 1) {
|
|
1211
|
+
kept = kept.slice(1)
|
|
1212
|
+
payload = { ...payload, turns: kept }
|
|
1213
|
+
json = JSON.stringify(payload)
|
|
1214
|
+
}
|
|
1215
|
+
return json
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export function unpackConversation(raw) {
|
|
1219
|
+
if (!raw) return null
|
|
1220
|
+
let payload
|
|
1221
|
+
try {
|
|
1222
|
+
payload = JSON.parse(raw)
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
return null
|
|
1225
|
+
}
|
|
1226
|
+
// A payload from a different format is not worth guessing at; starting
|
|
1227
|
+
// fresh is better than rendering something half-understood.
|
|
1228
|
+
if (!payload || payload.v !== CONVERSATION_FORMAT || !Array.isArray(payload.turns)) return null
|
|
1229
|
+
|
|
1230
|
+
const turns = payload.turns.filter((t) => t && typeof t.content === "string" &&
|
|
1231
|
+
(t.role === "user" || t.role === "assistant"))
|
|
1232
|
+
return { turns, open: !!payload.open, model: payload.model || null, savedAt: payload.savedAt || 0 }
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Storage can throw — private windows, blocked site data, quota — and none of
|
|
1236
|
+
// that is a reason for the widget to stop working.
|
|
1237
|
+
export function createConversationStore({ storage, key }) {
|
|
1238
|
+
const safely = (fn, fallback = null) => {
|
|
1239
|
+
try {
|
|
1240
|
+
return fn()
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
return fallback
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return {
|
|
1247
|
+
key,
|
|
1248
|
+
save(state) { return safely(() => { storage.setItem(key, packConversation(state)); return true }, false) },
|
|
1249
|
+
load() { return safely(() => unpackConversation(storage.getItem(key))) },
|
|
1250
|
+
clear() { return safely(() => { storage.removeItem(key); return true }, false) }
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
export function conversationKeyFor(location, prefix = "llm_meta_widget") {
|
|
1255
|
+
// Path only: a form submit returns to the same page with a new query string,
|
|
1256
|
+
// and that conversation is still the same conversation.
|
|
1257
|
+
return `${prefix}:${location.pathname}`
|
|
1258
|
+
}
|
|
@@ -452,7 +452,8 @@
|
|
|
452
452
|
import { runChatLoop, fetchMcpManifest, listMcpPrompts, getMcpPrompt,
|
|
453
453
|
listMcpResources, readMcpResource, promptMessagesToText,
|
|
454
454
|
loadHostResource, resourceLinesForTurn, resolvePromptArguments,
|
|
455
|
-
promptButtonProps, promptArgumentSummary, fetchOllamaModels
|
|
455
|
+
promptButtonProps, promptArgumentSummary, fetchOllamaModels,
|
|
456
|
+
createConversationStore, conversationKeyFor } from "<%= orchestrator_path %>";
|
|
456
457
|
import { marked } from "/llm_meta_widget_assets/marked.esm.js";
|
|
457
458
|
|
|
458
459
|
// Standard prose settings — GFM (tables, autolinks, strikethrough),
|
|
@@ -576,7 +577,26 @@
|
|
|
576
577
|
ro.observe(root);
|
|
577
578
|
}
|
|
578
579
|
|
|
579
|
-
|
|
580
|
+
// [{role, content, tools}] — grows across turns. `tools` is for the
|
|
581
|
+
// transcript only; the model is sent role and content.
|
|
582
|
+
var conversation = [];
|
|
583
|
+
|
|
584
|
+
// A page action that navigates used to take the conversation with it. The
|
|
585
|
+
// transcript lives in sessionStorage — per tab, gone when the tab closes —
|
|
586
|
+
// keyed by path, so submitting a form and landing back on the same page
|
|
587
|
+
// with a new query string keeps the thread.
|
|
588
|
+
var conversationStore = createConversationStore({
|
|
589
|
+
storage: window.sessionStorage,
|
|
590
|
+
key: conversationKeyFor(window.location)
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
function persistConversation() {
|
|
594
|
+
conversationStore.save({
|
|
595
|
+
turns: conversation,
|
|
596
|
+
open: !root.classList.contains("lmw-collapsed"),
|
|
597
|
+
model: MODEL
|
|
598
|
+
});
|
|
599
|
+
}
|
|
580
600
|
var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
|
|
581
601
|
var localTools = actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
|
|
582
602
|
var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
|
|
@@ -845,6 +865,10 @@
|
|
|
845
865
|
var RESOURCE_BUDGET_BYTES = 8000;
|
|
846
866
|
|
|
847
867
|
|
|
868
|
+
// Before anything asynchronous: if this page was navigated away from and
|
|
869
|
+
// back, the visitor should see their conversation immediately.
|
|
870
|
+
restoreConversation();
|
|
871
|
+
|
|
848
872
|
var wellKnownReady = (async function() {
|
|
849
873
|
var urls = WELL_KNOWN_URLS === null
|
|
850
874
|
? [ window.location.origin + "/.well-known/mcp.json" ]
|
|
@@ -921,11 +945,39 @@
|
|
|
921
945
|
promptsEl.style.display = "";
|
|
922
946
|
}
|
|
923
947
|
|
|
948
|
+
// Put a saved transcript back on screen: the same bubbles, the same
|
|
949
|
+
// markdown, and the same record of which tools ran. Called once at boot,
|
|
950
|
+
// before the welcome block, which is only for a genuinely fresh start.
|
|
951
|
+
function restoreConversation() {
|
|
952
|
+
var saved = conversationStore.load();
|
|
953
|
+
if (!saved || !saved.turns.length) return false;
|
|
954
|
+
|
|
955
|
+
conversation = saved.turns;
|
|
956
|
+
saved.turns.forEach(function(turn) {
|
|
957
|
+
var body = appendTurn(turn.role, turn.role === "assistant" ? "" : turn.content);
|
|
958
|
+
if (turn.role === "assistant") renderMarkdownInto(body, turn.content || "");
|
|
959
|
+
if (!turn.tools || !turn.tools.length) return;
|
|
960
|
+
var row = document.createElement("div");
|
|
961
|
+
row.className = "lmw-tool-chips";
|
|
962
|
+
turn.tools.forEach(function(tool) {
|
|
963
|
+
var chip = document.createElement("span");
|
|
964
|
+
chip.className = "lmw-tool-chip" + (tool.error ? " error" : "");
|
|
965
|
+
chip.textContent = (tool.error ? "❌ " : "🔧 ") + tool.name;
|
|
966
|
+
row.appendChild(chip);
|
|
967
|
+
});
|
|
968
|
+
if (body.parentNode) body.parentNode.appendChild(row);
|
|
969
|
+
});
|
|
970
|
+
historyEl.scrollTop = historyEl.scrollHeight;
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
|
|
924
974
|
// A blank panel tells a first-time visitor nothing. Open with a greeting
|
|
925
975
|
// and the offers themselves — each template showing what it will take from
|
|
926
976
|
// the page and what it will ask for — so the assistant is the page's way
|
|
927
977
|
// in rather than a box you must already know how to talk to.
|
|
928
978
|
function renderWelcome() {
|
|
979
|
+
// A restored transcript is not a fresh start; the greeting would push
|
|
980
|
+
// the conversation the visitor is mid-way through off the screen.
|
|
929
981
|
if (!historyEl || historyEl.querySelector(".message")) return;
|
|
930
982
|
historyEl.textContent = "";
|
|
931
983
|
|
|
@@ -1238,6 +1290,7 @@
|
|
|
1238
1290
|
clearBtn.addEventListener("click", function() {
|
|
1239
1291
|
if (currentAbort) { try { currentAbort.abort(); } catch (e) { /* noop */ } }
|
|
1240
1292
|
conversation = [];
|
|
1293
|
+
conversationStore.clear();
|
|
1241
1294
|
historyEl.innerHTML = "";
|
|
1242
1295
|
renderWelcome();
|
|
1243
1296
|
});
|
|
@@ -1280,7 +1333,7 @@
|
|
|
1280
1333
|
|
|
1281
1334
|
var resourceLines = await resourceLinesForThisTurn();
|
|
1282
1335
|
var messages = [{ role: "system", content: currentSystemPrompt(resourceLines) }]
|
|
1283
|
-
.concat(conversation)
|
|
1336
|
+
.concat(conversation.map(function(t) { return { role: t.role, content: t.content }; }))
|
|
1284
1337
|
.concat([{ role: "user", content: userText }]);
|
|
1285
1338
|
|
|
1286
1339
|
var result = await runChatLoop({
|
|
@@ -1331,8 +1384,17 @@
|
|
|
1331
1384
|
historyEl.scrollTop = historyEl.scrollHeight;
|
|
1332
1385
|
}
|
|
1333
1386
|
});
|
|
1334
|
-
conversation.push({ role: "user",
|
|
1335
|
-
conversation.push({
|
|
1387
|
+
conversation.push({ role: "user", content: userText });
|
|
1388
|
+
conversation.push({
|
|
1389
|
+
role: "assistant",
|
|
1390
|
+
content: result.content,
|
|
1391
|
+
// Kept so a restored transcript still shows what ran — losing
|
|
1392
|
+
// that record is what made a navigating action unbearable.
|
|
1393
|
+
tools: result.dispatched.map(function(d) {
|
|
1394
|
+
return d.error ? { name: d.toolCall.name, error: true } : { name: d.toolCall.name };
|
|
1395
|
+
})
|
|
1396
|
+
});
|
|
1397
|
+
persistConversation();
|
|
1336
1398
|
|
|
1337
1399
|
// Anything still pending never reported an outcome (a class that
|
|
1338
1400
|
// does not round-trip, or a dispatch that vanished). Reconcile
|