@nqminds/mcp-client 1.0.28 → 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.
- package/dist/MCPChat.d.ts.map +1 -1
- package/dist/MCPChat.js +224 -3
- package/dist/styles/MCPChat.css +19 -0
- package/package.json +1 -1
package/dist/MCPChat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MCPChat.d.ts","sourceRoot":"","sources":["../src/MCPChat.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;
|
|
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",
|
|
@@ -96,7 +308,15 @@ https://find-and-update.company-information.service.gov.uk/company/[companies_ho
|
|
|
96
308
|
2.1.3.8. Are they a director, PSC or investor of one of the other directors, PSCs or investors of the company?
|
|
97
309
|
2.1.3.9. Are there possible family relationships between directors, PSCs or investors?
|
|
98
310
|
3. Check whether dissolved entities registered at the same address share directors, PSCs or investors with the company
|
|
99
|
-
4.
|
|
311
|
+
4. Use the get_company_circular_ownership tool to check whether the company has any circular ownership patterns. Also use get_ownership_analysis to gather the full ownership structure.
|
|
312
|
+
4.1. Present the full ownership structure as a table (Name | Company no. | Kind | Ownership % | Control type | Notified on). This must appear regardless of whether circular ownership is found.
|
|
313
|
+
4.2. If circular ownership IS detected:
|
|
314
|
+
4.2.1. It MUST be the very first section of the entire response, rendered as the CIRCULAR OWNERSHIP WARNING BLOCK described in the formatting rules. Do not place any other content before it.
|
|
315
|
+
4.2.2. Each detected cycle must have its own row in the cycle table inside the warning block, showing the full path with company numbers (e.g. AMCOR HOLDING (04227427) → AFP EUROPE (03051270) → AMCOR HOLDING (04227427)), cycle length, and ownership percentages in both directions.
|
|
316
|
+
4.2.3. After the closing --- of the warning block, continue with the ownership analysis table and then the rest of the response.
|
|
317
|
+
4.2.4. In the final risk summary table (step 5), include circular ownership as a separate HIGH-rated row.
|
|
318
|
+
4.3. If no circular ownership is found, add a brief ✅ note: "No circular ownership detected" before the ownership analysis table.
|
|
319
|
+
5. Your response should include a clear summary of each risk factor identified as a table with columns: Risk factor | Rating | Why / Immediate effect | Recommended next action. Ensure there is a blank line before the opening | of this table.
|
|
100
320
|
|
|
101
321
|
Note that your database does not list dissolved companies so you will need to search for those on Companies House. To gather additional information on people, use Companies House people search. The same person might appear in the search results multiple times with the same name so reconcile on birth date or address.
|
|
102
322
|
|
|
@@ -491,10 +711,11 @@ export function MCPChat({ companyNumber, apiEndpoint = "/api/mcp/chat", customSt
|
|
|
491
711
|
msg.role === "assistant" ? (React.createElement("div", { className: "mcp-chat-message-content markdown-content" },
|
|
492
712
|
React.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm], components: {
|
|
493
713
|
a: ({ href, children }) => (React.createElement("a", { href: href, target: "_blank", rel: "noopener noreferrer" }, children)),
|
|
494
|
-
} }, 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)),
|
|
495
715
|
msg.role === "assistant" && !msg.isStreaming && (React.createElement("div", { className: "mcp-chat-message-timestamp" },
|
|
496
716
|
msg.timestamp.toLocaleTimeString(),
|
|
497
|
-
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"))))))),
|
|
498
719
|
isLoading && (React.createElement("div", { className: "mcp-chat-message mcp-chat-message-assistant" },
|
|
499
720
|
React.createElement("div", { className: "mcp-chat-thinking" },
|
|
500
721
|
React.createElement("div", { className: "mcp-chat-thinking-title" },
|
package/dist/styles/MCPChat.css
CHANGED
|
@@ -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
|
─────────────────────────────────────────────── */
|