@nqminds/mcp-client 1.0.29 → 1.0.30

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.
@@ -1 +1 @@
1
- {"version":3,"file":"MCPChat.d.ts","sourceRoot":"","sources":["../src/MCPChat.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAGxE,OAAO,KAAK,EAAyB,YAAY,EAAe,MAAM,SAAS,CAAC;AAkNhF,wBAAgB,OAAO,CAAC,EACtB,aAAa,EACb,WAA6B,EAC7B,YAAiB,EACjB,SAAc,GACf,EAAE,YAAY,qBAshBd"}
1
+ {"version":3,"file":"MCPChat.d.ts","sourceRoot":"","sources":["../src/MCPChat.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAIxE,OAAO,KAAK,EAAyB,YAAY,EAAe,MAAM,SAAS,CAAC;AAwahF,wBAAgB,OAAO,CAAC,EACtB,aAAa,EACb,WAA6B,EAC7B,YAAiB,EACjB,SAAc,GACf,EAAE,YAAY,qBA6hBd"}
package/dist/MCPChat.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import React, { useState, useRef, useEffect, useCallback } from "react";
3
3
  import ReactMarkdown from "react-markdown";
4
4
  import remarkGfm from "remark-gfm";
5
+ import { renderToStaticMarkup } from "react-dom/server";
5
6
  /**
6
7
  * Strips utm_source=openai (and any surrounding & or ?) from a URL.
7
8
  */
@@ -10,6 +11,84 @@ function stripUtmSource(url) {
10
11
  .replace(/[?&]utm_source=openai/gi, (match) => (match.startsWith("?") ? "?" : ""))
11
12
  .replace(/\?$/, "");
12
13
  }
14
+ /**
15
+ * Fixes broken markdown tables where row-separating newlines are missing.
16
+ * Detects the separator row (|---|---|) to infer column count, then uses
17
+ * pipe-counting to split the flattened table text back into proper rows.
18
+ */
19
+ function fixBrokenTables(content) {
20
+ // 1) Rejoin separator rows that were split across line breaks
21
+ // e.g. "|---|---\n:|---|" → "|---|---:|---|"
22
+ content = content.replace(/(\|[-:| ]*-)[ \t]*\r?\n[ \t]*([-:| ]*\|)/g, (full, p1, p2) => {
23
+ const joined = p1 + p2;
24
+ if (/^\|[ :]*-{2,}[ :]*(\|[ :]*-{2,}[ :]*)+\|$/.test(joined.trim()))
25
+ return joined;
26
+ return full;
27
+ });
28
+ // 2) Scan line-by-line for inline separator patterns (= broken table)
29
+ const SEP_RE = /\|[ :]*-{2,}[ :]*(?:\|[ :]*-{2,}[ :]*)+\|/;
30
+ const lines = content.split("\n");
31
+ const out = [];
32
+ let i = 0;
33
+ while (i < lines.length) {
34
+ const line = lines[i];
35
+ const sepMatch = line.match(SEP_RE);
36
+ // No separator on this line, or the line IS just the separator → fine
37
+ if (!sepMatch || line.trim() === sepMatch[0]) {
38
+ out.push(line);
39
+ i++;
40
+ continue;
41
+ }
42
+ // Broken table: separator is inline with other content
43
+ const sep = sepMatch[0];
44
+ const cols = sep.split("|").filter((s) => /^[ :]*-+[ :]*$/.test(s)).length;
45
+ if (cols < 2) {
46
+ out.push(line);
47
+ i++;
48
+ continue;
49
+ }
50
+ // Absorb continuation lines that look like table content (start with |)
51
+ let raw = line;
52
+ i++;
53
+ while (i < lines.length) {
54
+ const next = lines[i].trim();
55
+ if (!next || !next.startsWith("|"))
56
+ break;
57
+ raw += " " + next;
58
+ i++;
59
+ }
60
+ // Flatten whitespace and locate every unescaped pipe
61
+ const flat = raw.replace(/\s+/g, " ").trim();
62
+ const pipes = [];
63
+ for (let k = 0; k < flat.length; k++) {
64
+ if (flat[k] === "|" && (k === 0 || flat[k - 1] !== "\\"))
65
+ pipes.push(k);
66
+ }
67
+ const perRow = cols + 1; // pipes per table row
68
+ const fullRows = Math.floor(pipes.length / perRow);
69
+ if (fullRows < 2) {
70
+ out.push(raw);
71
+ continue;
72
+ } // can't reconstruct
73
+ // Text that precedes the first pipe (e.g. a heading label)
74
+ const prefix = flat.substring(0, pipes[0]).trim();
75
+ if (prefix) {
76
+ out.push(prefix);
77
+ out.push("");
78
+ }
79
+ // Emit each complete row on its own line
80
+ for (let r = 0; r < fullRows; r++) {
81
+ out.push(flat.substring(pipes[r * perRow], pipes[r * perRow + perRow - 1] + 1));
82
+ }
83
+ // Trailing text after last complete row
84
+ const trailing = flat.substring(pipes[fullRows * perRow - 1] + 1).trim();
85
+ if (trailing) {
86
+ out.push("");
87
+ out.push(trailing);
88
+ }
89
+ }
90
+ return out.join("\n");
91
+ }
13
92
  /**
14
93
  * Post-processes AI response content to ensure all links open in a new tab
15
94
  * and have tracking parameters removed.
@@ -27,6 +106,139 @@ function preprocessLinks(content) {
27
106
  // Strip utm params from plain markdown links [text](url)
28
107
  return step1.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, text, url) => `[${text}](${stripUtmSource(url)})`);
29
108
  }
109
+ /**
110
+ * Renders an assistant message as a styled HTML document and triggers
111
+ * the browser's print dialog (which offers "Save as PDF").
112
+ */
113
+ function downloadMessageAsPdf(markdownContent, companyNumber) {
114
+ const html = renderToStaticMarkup(React.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm] }, preprocessLinks(fixBrokenTables(markdownContent))));
115
+ const title = companyNumber
116
+ ? `FLAIR Report — ${companyNumber}`
117
+ : "FLAIR Report";
118
+ const now = new Date();
119
+ const timestamp = now.toLocaleDateString("en-GB", {
120
+ day: "2-digit", month: "long", year: "numeric",
121
+ }) + " at " + now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
122
+ const doc = `<!DOCTYPE html>
123
+ <html lang="en">
124
+ <head>
125
+ <meta charset="utf-8" />
126
+ <title>${title}</title>
127
+ <style>
128
+ @page { margin: 20mm 15mm; }
129
+ body {
130
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
131
+ font-size: 13px;
132
+ line-height: 1.6;
133
+ color: #1a1a1a;
134
+ max-width: 780px;
135
+ margin: 0 auto;
136
+ padding: 0;
137
+ }
138
+ .report-header {
139
+ border-bottom: 2px solid #2563eb;
140
+ padding-bottom: 10px;
141
+ margin-bottom: 20px;
142
+ }
143
+ .report-header h1 {
144
+ font-size: 20px;
145
+ margin: 0 0 4px 0;
146
+ color: #2563eb;
147
+ }
148
+ .report-header .meta {
149
+ font-size: 11px;
150
+ color: #666;
151
+ }
152
+ h1, h2, h3, h4 { margin-top: 18px; margin-bottom: 8px; }
153
+ h1 { font-size: 18px; }
154
+ h2 { font-size: 16px; border-bottom: 1px solid #e5e7eb; padding-bottom: 4px; }
155
+ h3 { font-size: 14px; }
156
+ p { margin: 0 0 8px 0; }
157
+ ul, ol { margin: 4px 0 8px 0; padding-left: 24px; }
158
+ li { margin-bottom: 3px; }
159
+ table {
160
+ border-collapse: collapse;
161
+ width: 100%;
162
+ margin: 10px 0;
163
+ font-size: 12px;
164
+ }
165
+ th, td {
166
+ border: 1px solid #d1d5db;
167
+ padding: 6px 8px;
168
+ text-align: left;
169
+ }
170
+ th {
171
+ background-color: #f3f4f6;
172
+ font-weight: 600;
173
+ }
174
+ tr:nth-child(even) { background-color: #f9fafb; }
175
+ code {
176
+ background: #f3f4f6;
177
+ padding: 1px 4px;
178
+ border-radius: 3px;
179
+ font-size: 12px;
180
+ }
181
+ pre {
182
+ background: #f3f4f6;
183
+ padding: 10px;
184
+ border-radius: 4px;
185
+ overflow-x: auto;
186
+ font-size: 12px;
187
+ }
188
+ pre code { background: none; padding: 0; }
189
+ blockquote {
190
+ border-left: 3px solid #d1d5db;
191
+ margin: 8px 0;
192
+ padding: 4px 12px;
193
+ color: #555;
194
+ }
195
+ a { color: #2563eb; text-decoration: none; }
196
+ hr { border: none; border-top: 1px solid #e5e7eb; margin: 16px 0; }
197
+ .report-footer {
198
+ margin-top: 30px;
199
+ padding-top: 10px;
200
+ border-top: 1px solid #e5e7eb;
201
+ font-size: 10px;
202
+ color: #999;
203
+ text-align: center;
204
+ }
205
+ </style>
206
+ </head>
207
+ <body>
208
+ <div class="report-header">
209
+ <h1>${title}</h1>
210
+ <div class="meta">Generated ${timestamp} by FLAIR AI</div>
211
+ </div>
212
+ ${html}
213
+ <div class="report-footer">
214
+ This report was generated by FLAIR AI and should be independently verified.
215
+ </div>
216
+ </body>
217
+ </html>`;
218
+ const printWindow = window.open("", "_blank");
219
+ if (!printWindow) {
220
+ // Popup blocked — fall back to blob download of HTML
221
+ const blob = new Blob([doc], { type: "text/html" });
222
+ const url = URL.createObjectURL(blob);
223
+ const a = document.createElement("a");
224
+ a.href = url;
225
+ a.download = `${title.replace(/[^a-zA-Z0-9 _-]/g, "")}.html`;
226
+ a.click();
227
+ URL.revokeObjectURL(url);
228
+ return;
229
+ }
230
+ printWindow.document.write(doc);
231
+ printWindow.document.close();
232
+ // Wait for content to render before triggering print
233
+ printWindow.addEventListener("load", () => printWindow.print());
234
+ // Fallback if load already fired
235
+ setTimeout(() => {
236
+ try {
237
+ printWindow.print();
238
+ }
239
+ catch { /* ignore */ }
240
+ }, 600);
241
+ }
30
242
  const DEFAULT_ACTIONS = [
31
243
  {
32
244
  label: "Company overview",
@@ -499,10 +711,11 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
499
711
  msg.role === "assistant" ? (React.createElement("div", { className: "mcp-chat-message-content markdown-content" },
500
712
  React.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm], components: {
501
713
  a: ({ href, children }) => (React.createElement("a", { href: href, target: "_blank", rel: "noopener noreferrer" }, children)),
502
- } }, preprocessLinks(msg.content)))) : (React.createElement("div", { className: "mcp-chat-message-content" }, msg.content)),
714
+ } }, preprocessLinks(fixBrokenTables(msg.content))))) : (React.createElement("div", { className: "mcp-chat-message-content" }, msg.content)),
503
715
  msg.role === "assistant" && !msg.isStreaming && (React.createElement("div", { className: "mcp-chat-message-timestamp" },
504
716
  msg.timestamp.toLocaleTimeString(),
505
- msg.tokenInfo && (React.createElement("span", { className: "mcp-chat-token-info" }, msg.tokenInfo)))))))),
717
+ msg.tokenInfo && (React.createElement("span", { className: "mcp-chat-token-info" }, msg.tokenInfo)),
718
+ React.createElement("button", { className: "mcp-btn-download-pdf", onClick: () => downloadMessageAsPdf(msg.content, companyNumber), title: "Download as PDF" }, "\uD83D\uDCC4 PDF"))))))),
506
719
  isLoading && (React.createElement("div", { className: "mcp-chat-message mcp-chat-message-assistant" },
507
720
  React.createElement("div", { className: "mcp-chat-thinking" },
508
721
  React.createElement("div", { className: "mcp-chat-thinking-title" },
@@ -465,6 +465,25 @@
465
465
  padding-left: 8px;
466
466
  }
467
467
 
468
+ .mcp-btn-download-pdf {
469
+ font-size: 11px;
470
+ font-weight: 600;
471
+ padding: 3px 10px;
472
+ border: 1px solid #2563eb;
473
+ border-radius: 4px;
474
+ background: #2563eb;
475
+ color: #fff;
476
+ cursor: pointer;
477
+ transition: background-color 0.15s, box-shadow 0.15s;
478
+ line-height: 1.4;
479
+ letter-spacing: 0.02em;
480
+ }
481
+
482
+ .mcp-btn-download-pdf:hover {
483
+ background: #1d4ed8;
484
+ box-shadow: 0 1px 4px rgba(37, 99, 235, 0.35);
485
+ }
486
+
468
487
  /* ───────────────────────────────────────────────
469
488
  Markdown
470
489
  ─────────────────────────────────────────────── */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nqminds/mcp-client",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "Reusable MCP client component with AI chat interface",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",