@aleph-alpha/chat-kit 3.1.3 → 3.2.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.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-LPrW7CSQ.js → CkInput.vue_vue_type_script_setup_true_lang-B-4xnH2c.js} +1 -1
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +63 -37
- package/dist/components/index.js +1 -1
- package/dist/composables/fileUpload/types.d.ts +3 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +3 -1
- package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
- package/dist/helpers/unified/remarkNormalizeMath.d.ts +24 -0
- package/dist/helpers/unified/remarkNormalizeMath.d.ts.map +1 -0
- package/dist/{index-DVxCNAs3.js → index-DAmWGYNH.js} +37 -5
- package/dist/index.js +3 -3
- package/dist/{useChatKitLabels-Ctz1dHpx.js → useChatKitLabels-D5gNv8se.js} +97 -1
- package/package.json +4 -3
- package/src/adapters/responses-api.spec.ts +156 -0
- package/src/adapters/responses-api.ts +125 -58
- package/src/composables/fileUpload/types.ts +6 -0
- package/src/composables/fileUpload/useFileUpload.ts +7 -1
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +74 -24
- package/src/composables/fileUpload/useFileUploadWrapper.ts +44 -4
- package/src/handlers/index.ts +4 -0
- package/src/handlers/useMarkdownParser.spec.ts +34 -0
- package/src/handlers/useMarkdownParser.ts +10 -1
- package/src/helpers/unified/remarkNormalizeMath.spec.ts +84 -0
- package/src/helpers/unified/remarkNormalizeMath.ts +134 -0
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
UploadEvent,
|
|
14
14
|
} from './types';
|
|
15
15
|
import { useFileUpload } from './useFileUpload';
|
|
16
|
+
import { defaultEndpoints } from './types';
|
|
16
17
|
|
|
17
18
|
function buildHttpClient(
|
|
18
19
|
baseUrl: string,
|
|
@@ -176,11 +177,21 @@ export function createDocumentUploadProvider(
|
|
|
176
177
|
};
|
|
177
178
|
|
|
178
179
|
const httpClient = buildHttpClient(baseUrl, defaultHeaders);
|
|
180
|
+
const endpoints = defaultEndpoints;
|
|
179
181
|
|
|
180
182
|
// ---- Search store resolution (one per user) ----
|
|
181
183
|
|
|
182
184
|
let resolvedStoreId: string | null = null;
|
|
183
185
|
let resolvePromise: Promise<string> | null = null;
|
|
186
|
+
const resolvedWorkflowIdByStore = new Map<string, string>();
|
|
187
|
+
|
|
188
|
+
async function createWorkflowForStore(storeId: string): Promise<string> {
|
|
189
|
+
const workflowRes = await httpClient.post<{ id: string }>(
|
|
190
|
+
endpoints.createWorkflow(storeId),
|
|
191
|
+
workflowConfig,
|
|
192
|
+
);
|
|
193
|
+
return workflowRes.id;
|
|
194
|
+
}
|
|
184
195
|
|
|
185
196
|
async function createSearchStoreWithWorkflow(): Promise<string> {
|
|
186
197
|
const name = `store-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
@@ -190,10 +201,8 @@ export function createDocumentUploadProvider(
|
|
|
190
201
|
});
|
|
191
202
|
const newStoreId = storeRes.id;
|
|
192
203
|
|
|
193
|
-
await
|
|
194
|
-
|
|
195
|
-
workflowConfig,
|
|
196
|
-
);
|
|
204
|
+
const workflowApiId = await createWorkflowForStore(newStoreId);
|
|
205
|
+
resolvedWorkflowIdByStore.set(newStoreId, workflowApiId);
|
|
197
206
|
|
|
198
207
|
return newStoreId;
|
|
199
208
|
}
|
|
@@ -207,6 +216,35 @@ export function createDocumentUploadProvider(
|
|
|
207
216
|
total: number;
|
|
208
217
|
}
|
|
209
218
|
|
|
219
|
+
interface WorkflowListResponse {
|
|
220
|
+
items: Array<{
|
|
221
|
+
id: string;
|
|
222
|
+
name: string;
|
|
223
|
+
created_at: string;
|
|
224
|
+
}>;
|
|
225
|
+
total: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function fetchOrCreateWorkflow(storeId: string): Promise<string> {
|
|
229
|
+
const cached = resolvedWorkflowIdByStore.get(storeId);
|
|
230
|
+
if (cached) return cached;
|
|
231
|
+
|
|
232
|
+
const response = await httpClient.get<WorkflowListResponse>(
|
|
233
|
+
endpoints.workflows(storeId),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (response.items.length > 0) {
|
|
237
|
+
const match = response.items.find((item) => item.name === workflowId);
|
|
238
|
+
const selected = match ?? response.items[0]!;
|
|
239
|
+
resolvedWorkflowIdByStore.set(storeId, selected.id);
|
|
240
|
+
return selected.id;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const workflowApiId = await createWorkflowForStore(storeId);
|
|
244
|
+
resolvedWorkflowIdByStore.set(storeId, workflowApiId);
|
|
245
|
+
return workflowApiId;
|
|
246
|
+
}
|
|
247
|
+
|
|
210
248
|
async function fetchOrCreateStore(): Promise<string> {
|
|
211
249
|
try {
|
|
212
250
|
const response =
|
|
@@ -308,7 +346,9 @@ export function createDocumentUploadProvider(
|
|
|
308
346
|
if (!files.length) return;
|
|
309
347
|
|
|
310
348
|
const userStoreId = await resolveSearchStore();
|
|
349
|
+
const workflowApiId = await fetchOrCreateWorkflow(userStoreId);
|
|
311
350
|
upload.setStoreId(userStoreId);
|
|
351
|
+
upload.setWorkflowId(workflowApiId);
|
|
312
352
|
storeId.value = userStoreId;
|
|
313
353
|
|
|
314
354
|
const conversationId = toValue(conversationIdRef);
|
package/src/handlers/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { useMessageHandler } from './useMessageHandler';
|
|
2
2
|
export { useMarkdownParser } from './useMarkdownParser';
|
|
3
|
+
export {
|
|
4
|
+
remarkNormalizeMath,
|
|
5
|
+
normalizeMathDelimiters,
|
|
6
|
+
} from '../helpers/unified/remarkNormalizeMath';
|
|
3
7
|
export {
|
|
4
8
|
provideChatKitLabels,
|
|
5
9
|
useChatKitLabels,
|
|
@@ -140,4 +140,38 @@ describe('useMarkdownParser', () => {
|
|
|
140
140
|
expect(html.value).not.toContain('class="katex"');
|
|
141
141
|
expect(html.value).toContain('$5');
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it('promotes inline `$$...$$` to block math via the normalizer plugin', () => {
|
|
145
|
+
const source = ref('Inline before $$E=mc^2$$ inline after');
|
|
146
|
+
const { html } = useMarkdownParser(source, ref(undefined));
|
|
147
|
+
|
|
148
|
+
expect(html.value).toContain('class="katex-display"');
|
|
149
|
+
expect(html.value).toContain('E=mc^2');
|
|
150
|
+
expect(html.value).toContain('Inline before');
|
|
151
|
+
expect(html.value).toContain('inline after');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('renders LaTeX-style `\\(...\\)` as inline math', () => {
|
|
155
|
+
const source = ref('The expression \\(a+b\\) is small.');
|
|
156
|
+
const { html } = useMarkdownParser(source, ref(undefined));
|
|
157
|
+
|
|
158
|
+
expect(html.value).toContain('class="katex"');
|
|
159
|
+
expect(html.value).not.toContain('class="katex-display"');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('renders LaTeX-style `\\[...\\]` as block math', () => {
|
|
163
|
+
const source = ref('See \\[a+b\\] above.');
|
|
164
|
+
const { html } = useMarkdownParser(source, ref(undefined));
|
|
165
|
+
|
|
166
|
+
expect(html.value).toContain('class="katex-display"');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not render `$$...$$` inside a fenced code block as math', () => {
|
|
170
|
+
const source = ref('```\n$$x$$\n```');
|
|
171
|
+
const { html } = useMarkdownParser(source, ref(undefined));
|
|
172
|
+
|
|
173
|
+
expect(html.value).not.toContain('class="katex"');
|
|
174
|
+
expect(html.value).toContain('<code>');
|
|
175
|
+
expect(html.value).toContain('$$x$$');
|
|
176
|
+
});
|
|
143
177
|
});
|
|
@@ -8,6 +8,7 @@ import rehypeKatex from 'rehype-katex';
|
|
|
8
8
|
import rehypeStringify from 'rehype-stringify';
|
|
9
9
|
import DOMPurify, { type Config } from 'dompurify';
|
|
10
10
|
import 'katex/dist/katex.min.css';
|
|
11
|
+
import { remarkNormalizeMath } from '../helpers/unified/remarkNormalizeMath';
|
|
11
12
|
|
|
12
13
|
type SyncProcessor = { processSync: (value: string) => { toString(): string } };
|
|
13
14
|
|
|
@@ -73,7 +74,15 @@ const SANITIZE_CONFIG: Config = {
|
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
function buildProcessor(plugins: PluggableList): SyncProcessor {
|
|
76
|
-
|
|
77
|
+
// `remarkNormalizeMath` wraps `this.parser`, so it must be registered after
|
|
78
|
+
// `remark-parse`. The wrap rewrites the markdown source before `remark-math`
|
|
79
|
+
// sees it, promoting single-line `$$…$$` and LaTeX `\(…\)` / `\[…\]` into
|
|
80
|
+
// delimiters that `remark-math` recognizes.
|
|
81
|
+
let proc = unified()
|
|
82
|
+
.use(remarkParse)
|
|
83
|
+
.use(remarkNormalizeMath)
|
|
84
|
+
.use(remarkGfm)
|
|
85
|
+
.use(remarkMath);
|
|
77
86
|
for (const plugin of plugins) {
|
|
78
87
|
proc = proc.use(plugin as any);
|
|
79
88
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { normalizeMathDelimiters } from '../../helpers/unified/remarkNormalizeMath';
|
|
2
|
+
|
|
3
|
+
describe('normalizeMathDelimiters', () => {
|
|
4
|
+
it('returns an empty string for empty input', () => {
|
|
5
|
+
expect(normalizeMathDelimiters('')).toBe('');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('leaves text without math untouched', () => {
|
|
9
|
+
const input = 'Hello world, nothing to see here.';
|
|
10
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('promotes inline `$$...$$` to a block math fence', () => {
|
|
14
|
+
const out = normalizeMathDelimiters('before $$E=mc^2$$ after');
|
|
15
|
+
expect(out).toBe('before \n\n$$\nE=mc^2\n$$\n\n after');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('promotes multiple inline `$$...$$` occurrences on the same line', () => {
|
|
19
|
+
const out = normalizeMathDelimiters('$$a$$ and $$b$$');
|
|
20
|
+
expect(out).toBe('\n\n$$\na\n$$\n\n and \n\n$$\nb\n$$\n\n');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('leaves a properly fenced multi-line block math expression unchanged', () => {
|
|
24
|
+
const input = '$$\n\\int_0^1 x^2 dx\n$$';
|
|
25
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('converts LaTeX-style `\\(...\\)` to inline math', () => {
|
|
29
|
+
const out = normalizeMathDelimiters('The expression \\(a+b\\) is small.');
|
|
30
|
+
expect(out).toBe('The expression $a+b$ is small.');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('converts LaTeX-style `\\[...\\]` to a block math fence', () => {
|
|
34
|
+
const out = normalizeMathDelimiters('See \\[a+b\\] above.');
|
|
35
|
+
expect(out).toBe('See \n\n$$\na+b\n$$\n\n above.');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('handles multi-line `\\[...\\]` bodies', () => {
|
|
39
|
+
const out = normalizeMathDelimiters('\\[\n a+b \n\\]');
|
|
40
|
+
expect(out).toBe('\n\n$$\na+b\n$$\n\n');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not transform `$$...$$` inside a fenced code block', () => {
|
|
44
|
+
const input = '```\n$$x$$\n```';
|
|
45
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('does not transform `$$...$$` inside a tilde-fenced code block', () => {
|
|
49
|
+
const input = '~~~\n$$x$$\n~~~';
|
|
50
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('does not transform `$$...$$` inside an inline code span', () => {
|
|
54
|
+
const input = 'see `$$x$$` here';
|
|
55
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not transform LaTeX delimiters inside a fenced code block', () => {
|
|
59
|
+
const input = '```\nuse \\(x\\) and \\[y\\] verbatim\n```';
|
|
60
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('still transforms math outside the code block when both coexist', () => {
|
|
64
|
+
const input = '```\n$$inside$$\n```\noutside $$x$$ after';
|
|
65
|
+
const out = normalizeMathDelimiters(input);
|
|
66
|
+
expect(out).toContain('```\n$$inside$$\n```');
|
|
67
|
+
expect(out).toContain('\n\n$$\nx\n$$\n\n');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('leaves a lone dollar sign alone', () => {
|
|
71
|
+
const input = 'The price is $5.';
|
|
72
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('leaves two unrelated dollar amounts alone', () => {
|
|
76
|
+
const input = 'It costs $5 and $10 in total.';
|
|
77
|
+
expect(normalizeMathDelimiters(input)).toBe(input);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('trims internal whitespace inside transformed `$$...$$`', () => {
|
|
81
|
+
const out = normalizeMathDelimiters('$$ x + y $$');
|
|
82
|
+
expect(out).toBe('\n\n$$\nx + y\n$$\n\n');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Plugin } from 'unified';
|
|
2
|
+
|
|
3
|
+
const PLACEHOLDER_PREFIX = '\uE000PH_';
|
|
4
|
+
const PLACEHOLDER_SUFFIX = '\uE000';
|
|
5
|
+
const PLACEHOLDER_RE = /\uE000PH_(\d+)\uE000/g;
|
|
6
|
+
|
|
7
|
+
const FENCE_START_RE = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
8
|
+
const INLINE_CODE_RE = /`[^`\n]+`/g;
|
|
9
|
+
|
|
10
|
+
const LATEX_BLOCK_RE = /\\\[([\s\S]+?)\\\]/g;
|
|
11
|
+
const LATEX_INLINE_RE = /\\\(([^\n]+?)\\\)/g;
|
|
12
|
+
const SINGLE_LINE_DOUBLE_DOLLAR_RE = /\$\$([^\n$]+?)\$\$/g;
|
|
13
|
+
|
|
14
|
+
type Stash = (value: string) => string;
|
|
15
|
+
|
|
16
|
+
function isClosingFence(
|
|
17
|
+
line: string,
|
|
18
|
+
fenceChar: string,
|
|
19
|
+
minLen: number,
|
|
20
|
+
): boolean {
|
|
21
|
+
let i = 0;
|
|
22
|
+
let indent = 0;
|
|
23
|
+
while (
|
|
24
|
+
i < line.length &&
|
|
25
|
+
(line[i] === ' ' || line[i] === '\t') &&
|
|
26
|
+
indent < 4
|
|
27
|
+
) {
|
|
28
|
+
indent++;
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
if (indent >= 4) return false;
|
|
32
|
+
let runLen = 0;
|
|
33
|
+
while (i < line.length && line[i] === fenceChar) {
|
|
34
|
+
runLen++;
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
if (runLen < minLen) return false;
|
|
38
|
+
while (i < line.length) {
|
|
39
|
+
if (line[i] !== ' ' && line[i] !== '\t') return false;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Walk the source line-by-line and stash fenced code blocks (```/~~~) so
|
|
47
|
+
* their contents are not rewritten by subsequent math regexes. A line-based
|
|
48
|
+
* scanner avoids the catastrophic-backtracking risk of a single regex over the
|
|
49
|
+
* whole document.
|
|
50
|
+
*/
|
|
51
|
+
function protectFencedCode(input: string, stash: Stash): string {
|
|
52
|
+
const lines = input.split('\n');
|
|
53
|
+
const out: string[] = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < lines.length) {
|
|
56
|
+
const match = FENCE_START_RE.exec(lines[i]);
|
|
57
|
+
if (match) {
|
|
58
|
+
const fence = match[1];
|
|
59
|
+
const fenceChar = fence[0];
|
|
60
|
+
const minLen = fence.length;
|
|
61
|
+
const block: string[] = [lines[i]];
|
|
62
|
+
i++;
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
const current = lines[i];
|
|
65
|
+
block.push(current);
|
|
66
|
+
i++;
|
|
67
|
+
if (isClosingFence(current, fenceChar, minLen)) break;
|
|
68
|
+
}
|
|
69
|
+
out.push(stash(block.join('\n')));
|
|
70
|
+
} else {
|
|
71
|
+
out.push(lines[i]);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalize math delimiters in a markdown source string so they survive
|
|
80
|
+
* `remark-math`'s CommonMark-style parsing:
|
|
81
|
+
*
|
|
82
|
+
* - LaTeX-style block delimiters `\[ ... \]` become a fenced `$$ ... $$` block.
|
|
83
|
+
* - LaTeX-style inline delimiters `\( ... \)` become inline `$ ... $`.
|
|
84
|
+
* - Single-line `$$ ... $$` (which `remark-math` treats as inline math)
|
|
85
|
+
* is promoted to a proper block math fence on its own lines.
|
|
86
|
+
*
|
|
87
|
+
* Content inside fenced (``` / ~~~) or inline (`) code spans is preserved
|
|
88
|
+
* verbatim and is not subject to any of these rewrites.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeMathDelimiters(input: string): string {
|
|
91
|
+
if (!input) return input;
|
|
92
|
+
|
|
93
|
+
const placeholders: string[] = [];
|
|
94
|
+
const stash: Stash = (value) => {
|
|
95
|
+
const id = placeholders.length;
|
|
96
|
+
placeholders.push(value);
|
|
97
|
+
return `${PLACEHOLDER_PREFIX}${id}${PLACEHOLDER_SUFFIX}`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let s = protectFencedCode(input, stash);
|
|
101
|
+
s = s.replace(INLINE_CODE_RE, stash);
|
|
102
|
+
|
|
103
|
+
s = s.replace(
|
|
104
|
+
LATEX_BLOCK_RE,
|
|
105
|
+
(_m, body: string) => `\n\n$$\n${body.trim()}\n$$\n\n`,
|
|
106
|
+
);
|
|
107
|
+
s = s.replace(LATEX_INLINE_RE, (_m, body: string) => `$${body.trim()}$`);
|
|
108
|
+
s = s.replace(
|
|
109
|
+
SINGLE_LINE_DOUBLE_DOLLAR_RE,
|
|
110
|
+
(_m, body: string) => `\n\n$$\n${body.trim()}\n$$\n\n`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
s = s.replace(PLACEHOLDER_RE, (_m, id: string) => placeholders[Number(id)]);
|
|
114
|
+
return s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type ParserLike = (document: string, file: unknown) => unknown;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Unified plugin that wraps `this.parser` so the markdown source is run
|
|
121
|
+
* through {@link normalizeMathDelimiters} before parsing.
|
|
122
|
+
*
|
|
123
|
+
* Register this plugin *after* `remark-parse`, otherwise `this.parser` is not
|
|
124
|
+
* yet defined when the plugin is invoked at freeze time and the wrap is a
|
|
125
|
+
* no-op.
|
|
126
|
+
*/
|
|
127
|
+
export const remarkNormalizeMath: Plugin<[]> = function () {
|
|
128
|
+
const processor = this as unknown as { parser?: ParserLike };
|
|
129
|
+
const originalParser = processor.parser;
|
|
130
|
+
if (typeof originalParser !== 'function') return;
|
|
131
|
+
|
|
132
|
+
processor.parser = (document, file) =>
|
|
133
|
+
originalParser(normalizeMathDelimiters(document), file);
|
|
134
|
+
};
|