| 1 | using System.Text; |
| 2 | using System.Text.Json; |
| 3 | |
| 4 | namespace CascadeIDE.Features.WebAiPortal.Application; |
| 5 | |
| 6 | /// <summary> |
| 7 | /// Строит JavaScript для вставки UTF-8 текста в активный <c>textarea</c>/<c>input</c> или contenteditable после ответа моста ADR 0108 (эвристика сторонней страницы). |
| 8 | /// </summary> |
| 9 | public static class WebAiPortalComposerInjectScript |
| 10 | { |
| 11 | /// <summary>Возвращает IIFE со строкой JSON с полями <c>ok</c>, <c>reason</c>, опционально <c>tag</c>.</summary> |
| 12 | public static string Build(ReadOnlySpan<char> utf16Text) |
| 13 | { |
| 14 | var utf8Count = Encoding.UTF8.GetByteCount(utf16Text); |
| 15 | Span<byte> utf8 = utf8Count <= 1024 ? stackalloc byte[utf8Count] : new byte[utf8Count]; |
| 16 | _ = Encoding.UTF8.GetBytes(utf16Text, utf8); |
| 17 | |
| 18 | var b64 = Convert.ToBase64String(utf8); |
| 19 | var b64Js = JsonSerializer.Serialize(b64); |
| 20 | |
| 21 | return """ |
| 22 | (() => { |
| 23 | const b64 = %%B64%%; |
| 24 | let t; |
| 25 | try { |
| 26 | const bin = atob(b64); |
| 27 | const bytes = new Uint8Array(bin.length); |
| 28 | for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); |
| 29 | t = new TextDecoder("utf-8").decode(bytes); |
| 30 | } catch (e) { |
| 31 | return JSON.stringify({ ok: false, reason: "decode" }); |
| 32 | } |
| 33 | const sep = "\n\n"; |
| 34 | const ins = sep + t; |
| 35 | const el = document.activeElement; |
| 36 | if (!el) return JSON.stringify({ ok: false, reason: "no_focus" }); |
| 37 | const tag = el.tagName || ""; |
| 38 | if (tag === "TEXTAREA") { |
| 39 | const ta = /** @type {HTMLTextAreaElement} */ (el); |
| 40 | const start = typeof ta.selectionStart === "number" ? ta.selectionStart : ta.value.length; |
| 41 | const end = typeof ta.selectionEnd === "number" ? ta.selectionEnd : ta.value.length; |
| 42 | ta.value = ta.value.slice(0, start) + ins + ta.value.slice(end); |
| 43 | const pos = start + ins.length; |
| 44 | ta.selectionStart = ta.selectionEnd = pos; |
| 45 | ta.dispatchEvent(new Event("input", { bubbles: true })); |
| 46 | ta.dispatchEvent(new Event("change", { bubbles: true })); |
| 47 | return JSON.stringify({ ok: true, tag: "TEXTAREA" }); |
| 48 | } |
| 49 | if (tag === "INPUT") { |
| 50 | const inp = /** @type {HTMLInputElement} */ (el); |
| 51 | if (inp.type && inp.type !== "text" && inp.type !== "search") |
| 52 | return JSON.stringify({ ok: false, reason: "unsupported_input_type", tag: inp.type }); |
| 53 | const start = typeof inp.selectionStart === "number" ? inp.selectionStart : inp.value.length; |
| 54 | const end = typeof inp.selectionEnd === "number" ? inp.selectionEnd : inp.value.length; |
| 55 | inp.value = inp.value.slice(0, start) + ins + inp.value.slice(end); |
| 56 | const pos = start + ins.length; |
| 57 | inp.selectionStart = inp.selectionEnd = pos; |
| 58 | inp.dispatchEvent(new Event("input", { bubbles: true })); |
| 59 | inp.dispatchEvent(new Event("change", { bubbles: true })); |
| 60 | return JSON.stringify({ ok: true, tag: "INPUT" }); |
| 61 | } |
| 62 | if (el.isContentEditable) { |
| 63 | document.execCommand("insertText", false, ins.startsWith(sep) ? ins.slice(sep.length) : ins); |
| 64 | return JSON.stringify({ ok: true, tag: "contenteditable" }); |
| 65 | } |
| 66 | return JSON.stringify({ ok: false, reason: "unsupported_focus", tag }); |
| 67 | })(); |
| 68 | """.Replace("%%B64%%", b64Js, StringComparison.Ordinal); |
| 69 | } |
| 70 | } |
| 71 | |