@manny-est/node-red-flowpilot 0.4.1 → 0.5.1
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 +42 -0
- package/PROJECT-OVERVIEW.md +7 -7
- package/README.md +14 -8
- package/USER-GUIDE.md +27 -10
- package/flowpilot-core.css +103 -0
- package/flowpilot.js +123 -146
- package/lib/build-core-script.js +44 -0
- package/lib/build-system-prompt.js +6 -0
- package/lib/core/apply-review.js +1926 -0
- package/lib/core/history.js +93 -0
- package/lib/core/init.js +1435 -0
- package/lib/core/main.js +1777 -0
- package/lib/core/markdown.js +165 -0
- package/lib/core/modes.js +1690 -0
- package/lib/core/redaction.js +165 -0
- package/lib/core/selection-context.js +211 -0
- package/lib/envelope.js +136 -0
- package/lib/modify-system-prompt.js +3 -1
- package/lib/provider-openai-compatible.js +112 -15
- package/lib/storage.js +4 -0
- package/package.json +2 -3
- package/flowpilot-core.js +0 -6427
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// ---- Markdown rendering -----------------------------------------------
|
|
2
|
+
// Chat bubbles render a small, safe subset of markdown. All text is
|
|
3
|
+
// HTML-escaped before any tags are introduced, and only the fixed set of
|
|
4
|
+
// tags this code itself emits ever reaches the DOM — raw HTML from the
|
|
5
|
+
// model or the user is never interpreted, so no separate HTML sanitizer
|
|
6
|
+
// (e.g. DOMPurify) is needed.
|
|
7
|
+
function escapeHtml(str) {
|
|
8
|
+
return String(str)
|
|
9
|
+
.replace(/&/g, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, """)
|
|
13
|
+
.replace(/'/g, "'");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Inline markdown within a single (already-escaped) line: `code`,
|
|
17
|
+
// **bold**, *italic*, and [text](http(s) url) links. Code spans are
|
|
18
|
+
// split out first so their contents are immune to further markup.
|
|
19
|
+
function renderInlineMarkdown(escaped) {
|
|
20
|
+
var parts = escaped.split(/(`[^`]+`)/);
|
|
21
|
+
return parts.map(function (part, i) {
|
|
22
|
+
if (i % 2 === 1) {
|
|
23
|
+
return "<code>" + part.slice(1, -1) + "</code>";
|
|
24
|
+
}
|
|
25
|
+
return part
|
|
26
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
27
|
+
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
|
28
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
29
|
+
}).join("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Unique per-block id so the delegated copy-button handler (bound once,
|
|
33
|
+
// see content.find("#fp-messages").on("click", ".fp-code-copy", ...))
|
|
34
|
+
// can find the right <pre> — chat messages are injected as raw HTML
|
|
35
|
+
// strings via .html(), so a per-element .on() bind at construction time
|
|
36
|
+
// isn't possible here the way it is for the Generate review panel's
|
|
37
|
+
// JSON-tab copy button.
|
|
38
|
+
var nextCodeBlockId = 1;
|
|
39
|
+
|
|
40
|
+
// GFM table helpers, used by renderMarkdown below.
|
|
41
|
+
function isTableSeparatorRow(line) {
|
|
42
|
+
var trimmed = line.trim();
|
|
43
|
+
if (!trimmed) { return false; }
|
|
44
|
+
return /^\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?$/.test(trimmed);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function splitTableRow(line) {
|
|
48
|
+
var trimmed = line.trim();
|
|
49
|
+
if (trimmed.charAt(0) === "|") { trimmed = trimmed.slice(1); }
|
|
50
|
+
if (trimmed.charAt(trimmed.length - 1) === "|") { trimmed = trimmed.slice(0, -1); }
|
|
51
|
+
return trimmed.split("|").map(function (c) { return c.trim(); });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Block-level markdown: fenced code blocks, headings, tables,
|
|
55
|
+
// bullet/numbered lists, and paragraphs (consecutive lines joined with
|
|
56
|
+
// <br>).
|
|
57
|
+
function renderMarkdown(raw) {
|
|
58
|
+
var lines = String(raw || "").split("\n");
|
|
59
|
+
var html = "";
|
|
60
|
+
var listType = null;
|
|
61
|
+
var paraLines = [];
|
|
62
|
+
|
|
63
|
+
function flushPara() {
|
|
64
|
+
if (paraLines.length) {
|
|
65
|
+
html += "<p>" + paraLines.map(function (l) {
|
|
66
|
+
return renderInlineMarkdown(escapeHtml(l));
|
|
67
|
+
}).join("<br>") + "</p>";
|
|
68
|
+
paraLines = [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function closeList() {
|
|
72
|
+
if (listType) { html += "</" + listType + ">"; listType = null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var i = 0;
|
|
76
|
+
while (i < lines.length) {
|
|
77
|
+
var line = lines[i];
|
|
78
|
+
|
|
79
|
+
// Leading whitespace allowed: models commonly indent a fenced
|
|
80
|
+
// block nested under a numbered/bulleted list item (e.g. "1. Try
|
|
81
|
+
// this:\n ```bash\n curl ...\n ```"). An anchored-at-column-0
|
|
82
|
+
// regex misses that entirely, so the fence markers and everything
|
|
83
|
+
// inside fall through to plain paragraph text instead of a code
|
|
84
|
+
// block — exactly the "code blocks failed to load" bug.
|
|
85
|
+
var fence = line.match(/^\s*```(\w*)\s*$/);
|
|
86
|
+
if (fence) {
|
|
87
|
+
flushPara(); closeList();
|
|
88
|
+
var codeLines = [];
|
|
89
|
+
i++;
|
|
90
|
+
while (i < lines.length && !/^\s*```\s*$/.test(lines[i])) {
|
|
91
|
+
codeLines.push(lines[i]);
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
var codeBlockId = "fp-code-" + (nextCodeBlockId++);
|
|
95
|
+
html += "<div class=\"fp-code-toolbar\">" +
|
|
96
|
+
"<button class=\"fp-code-copy red-ui-button red-ui-button-small\" type=\"button\" data-code-id=\"" + codeBlockId + "\">Copy</button>" +
|
|
97
|
+
"</div>" +
|
|
98
|
+
"<pre id=\"" + codeBlockId + "\"><code>" + escapeHtml(codeLines.join("\n")) + "</code></pre>";
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
104
|
+
if (heading) {
|
|
105
|
+
flushPara(); closeList();
|
|
106
|
+
var level = Math.min(6, heading[1].length + 2);
|
|
107
|
+
html += "<h" + level + ">" + renderInlineMarkdown(escapeHtml(heading[2])) + "</h" + level + ">";
|
|
108
|
+
i++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// GFM-style pipe table: a header row immediately followed by a
|
|
113
|
+
// separator row (---/:--/--:), then zero or more body rows. Models
|
|
114
|
+
// reach for tables constantly in comparison-style answers; without
|
|
115
|
+
// this, every row just fell through to a paragraph line, showing
|
|
116
|
+
// the raw "| a | b |" syntax verbatim.
|
|
117
|
+
if (line.indexOf("|") !== -1 && i + 1 < lines.length && isTableSeparatorRow(lines[i + 1])) {
|
|
118
|
+
flushPara(); closeList();
|
|
119
|
+
var headerCells = splitTableRow(line);
|
|
120
|
+
i += 2;
|
|
121
|
+
var bodyRows = [];
|
|
122
|
+
while (i < lines.length && lines[i].trim() && lines[i].indexOf("|") !== -1) {
|
|
123
|
+
bodyRows.push(splitTableRow(lines[i]));
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
html += "<table><thead><tr>" +
|
|
127
|
+
headerCells.map(function (c) {
|
|
128
|
+
return "<th>" + renderInlineMarkdown(escapeHtml(c)) + "</th>";
|
|
129
|
+
}).join("") +
|
|
130
|
+
"</tr></thead><tbody>" +
|
|
131
|
+
bodyRows.map(function (row) {
|
|
132
|
+
return "<tr>" + row.map(function (c) {
|
|
133
|
+
return "<td>" + renderInlineMarkdown(escapeHtml(c)) + "</td>";
|
|
134
|
+
}).join("") + "</tr>";
|
|
135
|
+
}).join("") +
|
|
136
|
+
"</tbody></table>";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var bullet = line.match(/^\s*[-*]\s+(.*)$/);
|
|
141
|
+
if (bullet) {
|
|
142
|
+
flushPara();
|
|
143
|
+
if (listType !== "ul") { closeList(); html += "<ul>"; listType = "ul"; }
|
|
144
|
+
html += "<li>" + renderInlineMarkdown(escapeHtml(bullet[1])) + "</li>";
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var numbered = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
150
|
+
if (numbered) {
|
|
151
|
+
flushPara();
|
|
152
|
+
if (listType !== "ol") { closeList(); html += "<ol>"; listType = "ol"; }
|
|
153
|
+
html += "<li>" + renderInlineMarkdown(escapeHtml(numbered[1])) + "</li>";
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
flushPara(); closeList();
|
|
159
|
+
|
|
160
|
+
if (line.trim()) { paraLines.push(line); }
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
flushPara(); closeList();
|
|
164
|
+
return html;
|
|
165
|
+
}
|