micro-models-agent 2.4.0 → 2.8.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.
- package/CHANGELOG.md +109 -0
- package/README.md +88 -38
- package/bin/mma.mjs +41 -41
- package/dist/assets/app.js +246 -3032
- package/dist/assets/hljs.js +1244 -0
- package/dist/assets/index.html +15 -1
- package/dist/assets/js/api.js +28 -0
- package/dist/assets/js/dialog.js +105 -0
- package/dist/assets/js/dom.js +157 -0
- package/dist/assets/js/feed.js +995 -0
- package/dist/assets/js/format.js +114 -0
- package/dist/assets/js/i18n.js +46 -0
- package/dist/assets/js/settings.js +1126 -0
- package/dist/assets/js/store.js +79 -0
- package/dist/assets/js/tabs.js +672 -0
- package/dist/assets/md.js +277 -0
- package/dist/assets/setup.js +557 -0
- package/dist/assets/smd.js +1665 -0
- package/dist/assets/styles.css +271 -21
- package/dist/i18n/en.json +171 -3
- package/dist/i18n/ru.json +171 -3
- package/dist/main.js +4958 -2588
- package/dist/modules/browser/bridge-server.mjs +3 -3
- package/package.json +10 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Streaming Markdown for the web UI.
|
|
2
|
+
//
|
|
3
|
+
// Backed by `streaming-markdown` (smd.js, MIT, Damian Tarnawski) — an
|
|
4
|
+
// incremental parser that only appends to the DOM. Fenced code blocks are
|
|
5
|
+
// rendered with a header (language label + copy button) and highlighted once
|
|
6
|
+
// the fence closes (`highlight.js`, MIT), never per token.
|
|
7
|
+
//
|
|
8
|
+
// Safety: model output is untrusted. Text is always written with
|
|
9
|
+
// `createTextNode`; the only HTML that ever reaches the DOM is highlight.js
|
|
10
|
+
// output, which is parsed with `DOMParser` (inert) and imported node-by-node —
|
|
11
|
+
// no `innerHTML`. Link/image URLs are restricted to http(s)/mailto/data-image.
|
|
12
|
+
|
|
13
|
+
import * as smd from "./smd.js";
|
|
14
|
+
|
|
15
|
+
const COPY_ICON =
|
|
16
|
+
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>';
|
|
17
|
+
const CHECK_ICON = '<path d="M20 6 9 17l-5-5"/>';
|
|
18
|
+
|
|
19
|
+
function svgIcon(inner) {
|
|
20
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
21
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
22
|
+
svg.setAttribute("fill", "none");
|
|
23
|
+
svg.setAttribute("stroke", "currentColor");
|
|
24
|
+
svg.setAttribute("stroke-width", "2");
|
|
25
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
26
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
27
|
+
svg.setAttribute("aria-hidden", "true");
|
|
28
|
+
svg.setAttribute("focusable", "false");
|
|
29
|
+
svg.setAttribute("class", "svg-icon");
|
|
30
|
+
// The glyphs below are static, trusted literals (never model output).
|
|
31
|
+
const doc = new DOMParser().parseFromString("<svg>" + inner + "</svg>", "image/svg+xml");
|
|
32
|
+
const src = doc.documentElement;
|
|
33
|
+
for (const child of Array.from(src.childNodes)) {
|
|
34
|
+
svg.appendChild(document.importNode(child, true));
|
|
35
|
+
}
|
|
36
|
+
return svg;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setBtnIcon(btn, inner) {
|
|
40
|
+
btn.replaceChildren(svgIcon(inner));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Copy `text` to the clipboard. Resolves to `true` on success. */
|
|
44
|
+
export function copyText(text) {
|
|
45
|
+
const value = String(text == null ? "" : text);
|
|
46
|
+
if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
|
|
47
|
+
return navigator.clipboard.writeText(value).then(
|
|
48
|
+
() => true,
|
|
49
|
+
() => fallbackCopy(value),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return Promise.resolve(fallbackCopy(value));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Legacy path for non-secure origins (`mma web --host <lan-ip>`) and older
|
|
56
|
+
// browsers without the async Clipboard API.
|
|
57
|
+
function fallbackCopy(text) {
|
|
58
|
+
const ta = document.createElement("textarea");
|
|
59
|
+
ta.value = text;
|
|
60
|
+
ta.setAttribute("readonly", "");
|
|
61
|
+
ta.style.position = "fixed";
|
|
62
|
+
ta.style.top = "-1000px";
|
|
63
|
+
ta.style.opacity = "0";
|
|
64
|
+
document.body.appendChild(ta);
|
|
65
|
+
let ok = false;
|
|
66
|
+
try {
|
|
67
|
+
ta.select();
|
|
68
|
+
ok = document.execCommand("copy");
|
|
69
|
+
} catch (err) {
|
|
70
|
+
ok = false;
|
|
71
|
+
}
|
|
72
|
+
ta.remove();
|
|
73
|
+
return ok;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function nodesFromHtml(html) {
|
|
77
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
78
|
+
return Array.from(doc.body.childNodes).map((n) => document.importNode(n, true));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Highlight `codeEl` in place. Returns `true` when highlighting was applied. */
|
|
82
|
+
export function highlightInto(codeEl, code, lang) {
|
|
83
|
+
const hljs = window.hljs;
|
|
84
|
+
if (!hljs || !code) return false;
|
|
85
|
+
try {
|
|
86
|
+
const html =
|
|
87
|
+
lang && hljs.getLanguage(lang)
|
|
88
|
+
? hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
|
|
89
|
+
: hljs.highlightAuto(code).value;
|
|
90
|
+
codeEl.replaceChildren(...nodesFromHtml(html));
|
|
91
|
+
codeEl.classList.add("hljs");
|
|
92
|
+
return true;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safeUrl(raw, isImage) {
|
|
99
|
+
if (typeof raw !== "string" || !raw) return null;
|
|
100
|
+
const s = raw.trim();
|
|
101
|
+
if (isImage && /^data:image\/[a-z0-9.+-]+;/i.test(s)) return s;
|
|
102
|
+
try {
|
|
103
|
+
const u = new URL(s, location.href);
|
|
104
|
+
if (u.protocol === "http:" || u.protocol === "https:" || u.protocol === "mailto:") {
|
|
105
|
+
return u.href;
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
/* relative or malformed */
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function makeCopyButton(opts) {
|
|
114
|
+
const btn = document.createElement("button");
|
|
115
|
+
btn.type = "button";
|
|
116
|
+
btn.className = "cb-copy";
|
|
117
|
+
btn.disabled = true;
|
|
118
|
+
btn.setAttribute("aria-label", opts.copyLabel || "copy");
|
|
119
|
+
btn.title = opts.copyLabel || "copy";
|
|
120
|
+
setBtnIcon(btn, COPY_ICON);
|
|
121
|
+
return btn;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function flashButton(btn, inner, label, ms) {
|
|
125
|
+
const prevTitle = btn.title;
|
|
126
|
+
setBtnIcon(btn, inner);
|
|
127
|
+
if (label) {
|
|
128
|
+
btn.setAttribute("aria-label", label);
|
|
129
|
+
btn.title = label;
|
|
130
|
+
}
|
|
131
|
+
btn.classList.add("done");
|
|
132
|
+
window.setTimeout(() => {
|
|
133
|
+
setBtnIcon(btn, COPY_ICON);
|
|
134
|
+
btn.setAttribute("aria-label", prevTitle);
|
|
135
|
+
btn.title = prevTitle;
|
|
136
|
+
btn.classList.remove("done");
|
|
137
|
+
}, ms);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function wireCopy(btn, codeEl, opts) {
|
|
141
|
+
btn.addEventListener("click", () => {
|
|
142
|
+
if (btn.disabled) return;
|
|
143
|
+
copyText(codeEl.textContent).then((ok) => {
|
|
144
|
+
if (ok) flashButton(btn, CHECK_ICON, opts.copiedLabel || "copied", 1400);
|
|
145
|
+
else flashButton(btn, COPY_ICON, opts.copyFailedLabel || "copy failed", 1800);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function addToken(data, type) {
|
|
151
|
+
data.types.push(type);
|
|
152
|
+
if (type === smd.CODE_BLOCK || type === smd.CODE_FENCE) {
|
|
153
|
+
const parent = data.nodes[data.index];
|
|
154
|
+
const wrap = document.createElement("div");
|
|
155
|
+
wrap.className = "codeblock";
|
|
156
|
+
const head = document.createElement("div");
|
|
157
|
+
head.className = "cb-head";
|
|
158
|
+
const langEl = document.createElement("span");
|
|
159
|
+
langEl.className = "cb-lang";
|
|
160
|
+
const btn = makeCopyButton(data.opts);
|
|
161
|
+
head.appendChild(langEl);
|
|
162
|
+
head.appendChild(btn);
|
|
163
|
+
const pre = document.createElement("pre");
|
|
164
|
+
const codeEl = document.createElement("code");
|
|
165
|
+
pre.appendChild(codeEl);
|
|
166
|
+
wrap.appendChild(head);
|
|
167
|
+
wrap.appendChild(pre);
|
|
168
|
+
parent.appendChild(wrap);
|
|
169
|
+
data.nodes[++data.index] = codeEl;
|
|
170
|
+
data.code = { text: "", lang: "", codeEl, langEl, btn, wrap };
|
|
171
|
+
wireCopy(btn, codeEl, data.opts);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
smd.default_add_token(data, type);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function finalizeCode(data) {
|
|
178
|
+
const c = data.code;
|
|
179
|
+
if (!c) return;
|
|
180
|
+
if (data.opts.highlight !== false) highlightInto(c.codeEl, c.text, c.lang);
|
|
181
|
+
c.wrap.setAttribute("data-lang", c.lang || "");
|
|
182
|
+
c.wrap.classList.add("closed");
|
|
183
|
+
c.btn.disabled = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function endToken(data) {
|
|
187
|
+
const type = data.types.pop();
|
|
188
|
+
if (type === smd.CODE_BLOCK || type === smd.CODE_FENCE) {
|
|
189
|
+
finalizeCode(data);
|
|
190
|
+
data.code = null;
|
|
191
|
+
}
|
|
192
|
+
smd.default_end_token(data);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function addText(data, text) {
|
|
196
|
+
if (data.code) {
|
|
197
|
+
data.code.text += text;
|
|
198
|
+
data.code.codeEl.appendChild(document.createTextNode(text));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
smd.default_add_text(data, text);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function setAttr(data, type, value) {
|
|
205
|
+
if (data.code && type === smd.LANG) {
|
|
206
|
+
const lang = String(value || "").trim().toLowerCase().split(/\s+/)[0] || "";
|
|
207
|
+
data.code.lang = lang;
|
|
208
|
+
data.code.langEl.textContent = lang;
|
|
209
|
+
if (lang) data.code.codeEl.className = "language-" + lang;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (type === smd.HREF || type === smd.SRC) {
|
|
213
|
+
const node = data.nodes[data.index];
|
|
214
|
+
const url = safeUrl(value, type === smd.SRC);
|
|
215
|
+
if (url && node) {
|
|
216
|
+
if (type === smd.SRC) {
|
|
217
|
+
node.setAttribute("src", url);
|
|
218
|
+
node.setAttribute("alt", "");
|
|
219
|
+
} else {
|
|
220
|
+
node.setAttribute("href", url);
|
|
221
|
+
if (node.tagName === "A") {
|
|
222
|
+
node.setAttribute("target", "_blank");
|
|
223
|
+
node.setAttribute("rel", "noopener noreferrer");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
smd.default_set_attr(data, type, value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Mount a streaming Markdown renderer that appends a `.md` container to
|
|
234
|
+
* `parent`. Feed chunks with `write`, finish the segment with `end`.
|
|
235
|
+
*/
|
|
236
|
+
export function mountMarkdown(parent, opts) {
|
|
237
|
+
const options = opts || {};
|
|
238
|
+
const root = document.createElement("div");
|
|
239
|
+
root.className = "md";
|
|
240
|
+
parent.appendChild(root);
|
|
241
|
+
const data = {
|
|
242
|
+
root,
|
|
243
|
+
nodes: [root],
|
|
244
|
+
index: 0,
|
|
245
|
+
types: [],
|
|
246
|
+
code: null,
|
|
247
|
+
opts: options,
|
|
248
|
+
};
|
|
249
|
+
const parser = smd.parser({
|
|
250
|
+
data,
|
|
251
|
+
add_token: addToken,
|
|
252
|
+
end_token: endToken,
|
|
253
|
+
add_text: addText,
|
|
254
|
+
set_attr: setAttr,
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
root,
|
|
258
|
+
write(chunk) {
|
|
259
|
+
smd.parser_write(parser, String(chunk == null ? "" : chunk));
|
|
260
|
+
},
|
|
261
|
+
end() {
|
|
262
|
+
smd.parser_end(parser);
|
|
263
|
+
if (data.code) {
|
|
264
|
+
finalizeCode(data);
|
|
265
|
+
data.code = null;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Render a complete Markdown string into a fresh `.md` container. */
|
|
272
|
+
export function renderMarkdown(parent, text, opts) {
|
|
273
|
+
const m = mountMarkdown(parent, opts);
|
|
274
|
+
m.write(text);
|
|
275
|
+
m.end();
|
|
276
|
+
return m.root;
|
|
277
|
+
}
|