@abstractframework/panel-chat 0.1.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.
@@ -0,0 +1,246 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import "./panel_chat.css";
4
+ function highlightInline(text, state) {
5
+ const s = String(text ?? "");
6
+ if (!state || !state.needles.length)
7
+ return [s];
8
+ const out = [];
9
+ const hayLower = s.toLowerCase();
10
+ let pos = 0;
11
+ while (pos < s.length) {
12
+ let bestStart = -1;
13
+ let bestNeedle = "";
14
+ for (let i = 0; i < state.needlesLower.length; i++) {
15
+ const needleLower = state.needlesLower[i];
16
+ if (!needleLower)
17
+ continue;
18
+ const idx = hayLower.indexOf(needleLower, pos);
19
+ if (idx === -1)
20
+ continue;
21
+ const needle = state.needles[i] || "";
22
+ if (!needle)
23
+ continue;
24
+ if (bestStart === -1 || idx < bestStart || (idx === bestStart && needle.length > bestNeedle.length)) {
25
+ bestStart = idx;
26
+ bestNeedle = needle;
27
+ }
28
+ }
29
+ if (bestStart === -1 || !bestNeedle) {
30
+ out.push(s.slice(pos));
31
+ break;
32
+ }
33
+ if (bestStart > pos)
34
+ out.push(s.slice(pos, bestStart));
35
+ const id = state.hits === 0 && state.id ? state.id : undefined;
36
+ const matchedText = s.slice(bestStart, bestStart + bestNeedle.length);
37
+ out.push(_jsx("span", { id: id, className: state.className, children: matchedText }, `hl:${state.hits}`));
38
+ state.hits += 1;
39
+ pos = bestStart + bestNeedle.length;
40
+ }
41
+ return out;
42
+ }
43
+ function renderInline(text, highlight) {
44
+ const out = [];
45
+ const s = String(text ?? "");
46
+ let i = 0;
47
+ let buf = "";
48
+ const flush = () => {
49
+ if (!buf)
50
+ return;
51
+ for (const node of highlightInline(buf, highlight))
52
+ out.push(node);
53
+ buf = "";
54
+ };
55
+ while (i < s.length) {
56
+ const ch = s[i];
57
+ if (ch === "`") {
58
+ const j = s.indexOf("`", i + 1);
59
+ if (j !== -1) {
60
+ flush();
61
+ const inner = s.slice(i + 1, j);
62
+ out.push(_jsx("code", { children: highlight ? highlightInline(inner, highlight) : inner }, `code:${i}`));
63
+ i = j + 1;
64
+ continue;
65
+ }
66
+ }
67
+ if (ch === "*" && s[i + 1] === "*") {
68
+ const j = s.indexOf("**", i + 2);
69
+ if (j !== -1) {
70
+ flush();
71
+ const inner = s.slice(i + 2, j);
72
+ out.push(_jsx("strong", { children: highlight ? highlightInline(inner, highlight) : inner }, `bold:${i}`));
73
+ i = j + 2;
74
+ continue;
75
+ }
76
+ }
77
+ if (ch === "*" && s[i + 1] !== "*") {
78
+ const j = s.indexOf("*", i + 1);
79
+ if (j !== -1) {
80
+ flush();
81
+ const inner = s.slice(i + 1, j);
82
+ out.push(_jsx("em", { children: highlight ? highlightInline(inner, highlight) : inner }, `em:${i}`));
83
+ i = j + 1;
84
+ continue;
85
+ }
86
+ }
87
+ buf += ch;
88
+ i += 1;
89
+ }
90
+ flush();
91
+ return out;
92
+ }
93
+ function normalizeLines(text) {
94
+ const s = String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
95
+ return s.split("\n");
96
+ }
97
+ function splitTableRow(line) {
98
+ let s = String(line ?? "").trim();
99
+ if (!s)
100
+ return [];
101
+ if (s.startsWith("|"))
102
+ s = s.slice(1);
103
+ if (s.endsWith("|"))
104
+ s = s.slice(0, -1);
105
+ return s.split("|").map((c) => String(c ?? "").trim());
106
+ }
107
+ function isTableSeparator(line) {
108
+ let s = String(line ?? "").trim();
109
+ if (!s)
110
+ return false;
111
+ if (s.startsWith("|"))
112
+ s = s.slice(1);
113
+ if (s.endsWith("|"))
114
+ s = s.slice(0, -1);
115
+ const cells = s.split("|").map((c) => String(c ?? "").trim());
116
+ if (cells.length < 2)
117
+ return false;
118
+ return cells.every((c) => /^:?-{3,}:?$/.test(c));
119
+ }
120
+ export function Markdown({ text, className, highlight, highlights, highlightClassName, highlightId, }) {
121
+ const lines = normalizeLines(text);
122
+ const blocks = [];
123
+ const needlesRaw = [];
124
+ if (Array.isArray(highlights))
125
+ needlesRaw.push(...highlights);
126
+ if (typeof highlight === "string" && highlight.trim())
127
+ needlesRaw.push(highlight);
128
+ const needles = Array.from(new Set(needlesRaw.map((s) => String(s ?? "").trim()).filter(Boolean))).filter((n) => n.length >= 4);
129
+ const highlightState = needles.length
130
+ ? {
131
+ needles,
132
+ needlesLower: needles.map((n) => n.toLowerCase()),
133
+ className: highlightClassName || "pc-md_hl",
134
+ id: highlightId,
135
+ hits: 0,
136
+ }
137
+ : null;
138
+ let i = 0;
139
+ while (i < lines.length) {
140
+ const line = String(lines[i] ?? "");
141
+ if (!line.trim()) {
142
+ i += 1;
143
+ continue;
144
+ }
145
+ const hr = line.trim();
146
+ if (hr === "---" || hr === "___" || hr === "***" || /^(-{3,}|_{3,}|\*{3,})$/.test(hr)) {
147
+ blocks.push(_jsx("hr", { className: "pc-md_hr" }, `hr:${i}`));
148
+ i += 1;
149
+ continue;
150
+ }
151
+ if (line.trim().startsWith("```")) {
152
+ const fence = line.trim();
153
+ const lang = fence.replace(/```/g, "").trim();
154
+ const codeLines = [];
155
+ i += 1;
156
+ while (i < lines.length && !String(lines[i] ?? "").trim().startsWith("```")) {
157
+ codeLines.push(String(lines[i] ?? ""));
158
+ i += 1;
159
+ }
160
+ if (i < lines.length)
161
+ i += 1;
162
+ const code = codeLines.join("\n");
163
+ blocks.push(_jsx("pre", { className: "pc-md_pre", children: _jsx("code", { className: lang ? `language-${lang}` : undefined, children: code }) }, `pre:${i}`));
164
+ continue;
165
+ }
166
+ const headingM = line.match(/^(#{1,3})\s+(.*)$/);
167
+ if (headingM) {
168
+ const level = headingM[1].length;
169
+ const content = headingM[2] || "";
170
+ const nodes = renderInline(content, highlightState);
171
+ if (level === 1)
172
+ blocks.push(_jsx("h1", { children: nodes }, `h1:${i}`));
173
+ else if (level === 2)
174
+ blocks.push(_jsx("h2", { children: nodes }, `h2:${i}`));
175
+ else
176
+ blocks.push(_jsx("h3", { children: nodes }, `h3:${i}`));
177
+ i += 1;
178
+ continue;
179
+ }
180
+ if (line.trimStart().startsWith(">")) {
181
+ const quoteLines = [];
182
+ while (i < lines.length && String(lines[i] ?? "").trimStart().startsWith(">")) {
183
+ const raw = String(lines[i] ?? "");
184
+ const t = raw.trimStart();
185
+ quoteLines.push(t.replace(/^>\s?/, ""));
186
+ i += 1;
187
+ }
188
+ blocks.push(_jsx("blockquote", { className: "pc-md_quote", children: quoteLines.map((q, idx) => (_jsxs(React.Fragment, { children: [renderInline(q, highlightState), idx < quoteLines.length - 1 ? _jsx("br", {}) : null] }, `q:${i}:${idx}`))) }, `quote:${i}`));
189
+ continue;
190
+ }
191
+ if (line.includes("|") && i + 1 < lines.length && isTableSeparator(String(lines[i + 1] ?? ""))) {
192
+ const headers = splitTableRow(line);
193
+ const colCount = Math.max(1, headers.length);
194
+ const rows = [];
195
+ i += 2;
196
+ while (i < lines.length) {
197
+ const rowLine = String(lines[i] ?? "");
198
+ if (!rowLine.trim())
199
+ break;
200
+ if (!rowLine.includes("|"))
201
+ break;
202
+ const cells = splitTableRow(rowLine);
203
+ const normalized = [];
204
+ for (let c = 0; c < colCount; c++)
205
+ normalized.push(cells[c] ?? "");
206
+ rows.push(normalized);
207
+ i += 1;
208
+ }
209
+ blocks.push(_jsx("div", { className: "pc-md_table_wrap", children: _jsxs("table", { className: "pc-md_table", children: [_jsx("thead", { children: _jsx("tr", { children: headers.slice(0, colCount).map((h, idx) => (_jsx("th", { children: renderInline(h, highlightState) }, `th:${i}:${idx}`))) }) }), _jsx("tbody", { children: rows.map((r, rIdx) => (_jsx("tr", { children: r.map((cell, cIdx) => (_jsx("td", { children: renderInline(cell, highlightState) }, `td:${i}:${rIdx}:${cIdx}`))) }, `tr:${i}:${rIdx}`))) })] }) }, `table:${i}`));
210
+ continue;
211
+ }
212
+ const isOrdered = (s) => /^\s*\d+\.\s+/.test(s);
213
+ if (isOrdered(line)) {
214
+ const items = [];
215
+ while (i < lines.length && isOrdered(String(lines[i] ?? ""))) {
216
+ const t = String(lines[i] ?? "");
217
+ items.push(t.replace(/^\s*\d+\.\s+/, ""));
218
+ i += 1;
219
+ }
220
+ blocks.push(_jsx("ol", { className: "pc-md_ol", children: items.map((it, idx) => (_jsx("li", { children: renderInline(it, highlightState) }, `oli:${i}:${idx}`))) }, `ol:${i}`));
221
+ continue;
222
+ }
223
+ const isBullet = (s) => {
224
+ const t = s.trimStart();
225
+ return t.startsWith("- ") || t.startsWith("* ");
226
+ };
227
+ if (isBullet(line)) {
228
+ const items = [];
229
+ while (i < lines.length && isBullet(String(lines[i] ?? ""))) {
230
+ const t = String(lines[i] ?? "").trimStart();
231
+ items.push(t.slice(2));
232
+ i += 1;
233
+ }
234
+ blocks.push(_jsx("ul", { className: "pc-md_ul", children: items.map((it, idx) => (_jsx("li", { children: renderInline(it, highlightState) }, `li:${i}:${idx}`))) }, `ul:${i}`));
235
+ continue;
236
+ }
237
+ const paraLines = [];
238
+ while (i < lines.length && String(lines[i] ?? "").trim()) {
239
+ paraLines.push(String(lines[i] ?? ""));
240
+ i += 1;
241
+ }
242
+ blocks.push(_jsx("p", { className: "pc-md_p", children: paraLines.map((pl, idx) => (_jsxs(React.Fragment, { children: [renderInline(pl, highlightState), idx < paraLines.length - 1 ? _jsx("br", {}) : null] }, `pl:${i}:${idx}`))) }, `p:${i}`));
243
+ }
244
+ const cls = ["pc-md", className].filter(Boolean).join(" ");
245
+ return _jsx("div", { className: cls, children: blocks });
246
+ }
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import "./panel_chat.css";
3
+ export declare function ChatMessageContent(props: {
4
+ text: string;
5
+ className?: string;
6
+ renderMarkdown?: (markdown: string) => React.ReactElement;
7
+ jsonCollapseAfterDepth?: number;
8
+ }): React.ReactElement;
9
+ //# sourceMappingURL=message_content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message_content.d.ts","sourceRoot":"","sources":["../src/message_content.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,kBAAkB,CAAC;AAM1B,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,KAAK,CAAC,YAAY,CAAC;IAC1D,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,GAAG,KAAK,CAAC,YAAY,CAsBrB"}
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import "./panel_chat.css";
3
+ import { JsonViewer } from "./json_viewer";
4
+ import { Markdown } from "./markdown";
5
+ import { tryParseJson } from "./utils";
6
+ export function ChatMessageContent(props) {
7
+ const text = String(props.text ?? "");
8
+ const parsed = tryParseJson(text);
9
+ const cls = ["pc-chat-content", props.className].filter(Boolean).join(" ");
10
+ if (parsed !== null) {
11
+ return (_jsx("div", { className: cls, children: _jsx(JsonViewer, { value: parsed, collapseAfterDepth: props.jsonCollapseAfterDepth }) }));
12
+ }
13
+ if (props.renderMarkdown) {
14
+ return _jsx("div", { className: cls, children: props.renderMarkdown(text) });
15
+ }
16
+ return (_jsx("div", { className: cls, children: _jsx(Markdown, { text: text }) }));
17
+ }
@@ -0,0 +1,8 @@
1
+ export type PanelChatMessage = {
2
+ id?: string;
3
+ role: string;
4
+ content: string;
5
+ ts?: string;
6
+ title?: string;
7
+ };
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ export declare function tryParseJson(text: string): unknown | null;
2
+ export declare function chatToMarkdown(messages: Array<{
3
+ role: string;
4
+ content: string;
5
+ ts?: string;
6
+ title?: string;
7
+ }>, opts?: {
8
+ heading?: string;
9
+ }): string;
10
+ export declare function copyText(text: string): Promise<boolean>;
11
+ export declare function downloadTextFile(opts: {
12
+ filename: string;
13
+ text: string;
14
+ mime?: string;
15
+ }): void;
16
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CASzD;AA8BD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAuBnJ;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAqB7D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAc9F"}
package/dist/utils.js ADDED
@@ -0,0 +1,104 @@
1
+ export function tryParseJson(text) {
2
+ const trimmed = String(text ?? "").trim();
3
+ if (!trimmed)
4
+ return null;
5
+ if (!((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))))
6
+ return null;
7
+ try {
8
+ return JSON.parse(trimmed);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ function safeJson(value) {
15
+ if (value == null)
16
+ return "";
17
+ if (typeof value === "string")
18
+ return value;
19
+ try {
20
+ return JSON.stringify(value, null, 2);
21
+ }
22
+ catch {
23
+ return String(value);
24
+ }
25
+ }
26
+ function formatMessageHeader(m) {
27
+ const role = String(m.role || "message").trim() || "message";
28
+ const title = String(m.title || "").trim();
29
+ const ts = String(m.ts || "").trim();
30
+ const parts = [role];
31
+ if (title)
32
+ parts.push(title);
33
+ if (ts)
34
+ parts.push(ts);
35
+ return parts.join(" · ");
36
+ }
37
+ function ensureTrailingNewline(s) {
38
+ return s.endsWith("\n") ? s : `${s}\n`;
39
+ }
40
+ function isAlreadyFenced(content) {
41
+ return String(content || "").trimStart().startsWith("```");
42
+ }
43
+ export function chatToMarkdown(messages, opts) {
44
+ const heading = String(opts?.heading || "Chat").trim() || "Chat";
45
+ const lines = [`# ${heading}`, ""];
46
+ for (const m of messages) {
47
+ const header = formatMessageHeader(m);
48
+ lines.push(`## ${header}`, "");
49
+ const content = String(m.content || "");
50
+ const parsed = tryParseJson(content);
51
+ if (parsed !== null && !isAlreadyFenced(content)) {
52
+ lines.push("```json", safeJson(parsed).trimEnd(), "```", "");
53
+ }
54
+ else {
55
+ lines.push(ensureTrailingNewline(content).trimEnd(), "");
56
+ }
57
+ lines.push("---", "");
58
+ }
59
+ // Drop trailing divider/newlines.
60
+ while (lines.length && !lines[lines.length - 1].trim())
61
+ lines.pop();
62
+ if (lines.length && lines[lines.length - 1] === "---")
63
+ lines.pop();
64
+ return `${lines.join("\n").trimEnd()}\n`;
65
+ }
66
+ export async function copyText(text) {
67
+ const value = String(text || "");
68
+ if (!value)
69
+ return false;
70
+ try {
71
+ await navigator.clipboard.writeText(value);
72
+ return true;
73
+ }
74
+ catch {
75
+ try {
76
+ const el = document.createElement("textarea");
77
+ el.value = value;
78
+ el.style.position = "fixed";
79
+ el.style.left = "-9999px";
80
+ document.body.appendChild(el);
81
+ el.select();
82
+ document.execCommand("copy");
83
+ document.body.removeChild(el);
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ }
91
+ export function downloadTextFile(opts) {
92
+ const filename = String(opts.filename || "").trim() || "export.txt";
93
+ const text = String(opts.text || "");
94
+ const mime = String(opts.mime || "text/plain;charset=utf-8");
95
+ const blob = new Blob([text], { type: mime });
96
+ const url = URL.createObjectURL(blob);
97
+ const a = document.createElement("a");
98
+ a.href = url;
99
+ a.download = filename;
100
+ document.body.appendChild(a);
101
+ a.click();
102
+ a.remove();
103
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
104
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@abstractframework/panel-chat",
3
+ "version": "0.1.0",
4
+ "description": "Shared chat panel components for AbstractFramework (extracted from AbstractCode; reusable by other UIs).",
5
+ "type": "module",
6
+ "author": "Laurent-Philippe Albou",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "abstractframework",
10
+ "chat",
11
+ "ui",
12
+ "react"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/lpalbou/AbstractUIC.git",
17
+ "directory": "panel-chat"
18
+ },
19
+ "homepage": "https://github.com/lpalbou/AbstractUIC#readme",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./panel_chat.css": "./src/panel_chat.css"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src/panel_chat.css",
35
+ "README.md"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "peerDependencies": {
42
+ "@abstractframework/ui-kit": "^0.1.0",
43
+ "react": "^18.0.0",
44
+ "react-dom": "^18.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/react": "^18.2.48",
48
+ "@types/react-dom": "^18.2.18",
49
+ "typescript": "^5.3.3"
50
+ },
51
+ "sideEffects": [
52
+ "*.css"
53
+ ],
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ }
57
+ }