Minimal support for importing chat histories in Chub format Fix crasher in script.js:displayChats if user has directly put a Chub chat file into their user data Let eslint tidy a few things

a2d9526cdfa73d2e4eac0fe662479fe77c5e0e8f

ceruleandeep <deep@cerulean.navy>

2 files changed, +64 -23Ignore whitespace
public/script.js+3 -5
@@ -6923,15 +6923,13 @@ export async function displayPastChats() {
6923 }6923 }
6924 // Check whether `text` {string} includes all of the `fragments` {string[]}.6924 // Check whether `text` {string} includes all of the `fragments` {string[]}.
6925 function matchFragments(fragments, text) {6925 function matchFragments(fragments, text) {
6926 if (!text) {6926 if (!text || !text.toLowerCase) return false;
6927 return false;6927 return fragments.every(item => text.toLowerCase().includes(item));
6928 }
6929 return fragments.every(item => text.includes(item));
6930 }6928 }
6931 const fragments = makeQueryFragments(searchQuery);6929 const fragments = makeQueryFragments(searchQuery);
6932 // At least one chat message must match *all* the fragments.6930 // At least one chat message must match *all* the fragments.
6933 // Currently, this doesn't match if the fragment matches are distributed across several chat messages.6931 // Currently, this doesn't match if the fragment matches are distributed across several chat messages.
6934 return chatContent && Object.values(chatContent).some(message => matchFragments(fragments, message?.mes?.toLowerCase()));6932 return chatContent && Object.values(chatContent).some(message => matchFragments(fragments, message?.mes));
6935 });6933 });
69366934
6937 console.debug(filteredData);6935 console.debug(filteredData);
src/endpoints/chats.js+61 -18
@@ -92,8 +92,7 @@ function importOobaChat(userName, characterName, jsonData) {
92 }92 }
93 }93 }
9494
95 const chatContent = chat.map(obj => JSON.stringify(obj)).join('\n');95 return chat.map(obj => JSON.stringify(obj)).join('\n');
96 return chatContent;
97}96}
9897
99/**98/**
@@ -121,8 +120,7 @@ function importAgnaiChat(userName, characterName, jsonData) {
121 });120 });
122 }121 }
123122
124 const chatContent = chat.map(obj => JSON.stringify(obj)).join('\n');123 return chat.map(obj => JSON.stringify(obj)).join('\n');
125 return chatContent;
126}124}
127125
128/**126/**
@@ -159,6 +157,37 @@ function importCAIChat(userName, characterName, jsonData) {
159 return newChats;157 return newChats;
160}158}
161159
160/**
161 * Flattens `msg` and `swipes` data from Chub Chat format.
162 * Only changes enough to make it compatible with the standard chat serialization format.
163 * @param {string} userName User name
164 * @param {string} characterName Character name
165 * @param {string[]} lines serialised JSONL data
166 * @returns {string} Converted data
167 */
168function flattenChubChat(userName, characterName, lines) {
169 function flattenSwipe(swipe) {
170 return swipe.message ? swipe.message : swipe;
171 }
172
173 function convert(line) {
174 const lineData = tryParse(line);
175 if (!lineData) return line;
176
177 if (lineData.mes && lineData.mes.message) {
178 lineData.mes = lineData?.mes.message;
179 }
180
181 if (lineData?.swipes && Array.isArray(lineData.swipes)) {
182 lineData.swipes = lineData.swipes.map(swipe => flattenSwipe(swipe));
183 }
184
185 return JSON.stringify(lineData);
186 }
187
188 return (lines ?? []).map(convert).join('\n');
189}
190
162const router = express.Router();191const router = express.Router();
163192
164router.post('/save', jsonParser, function (request, response) {193router.post('/save', jsonParser, function (request, response) {
@@ -273,7 +302,7 @@ router.post('/export', jsonParser, async function (request, response) {
273 }302 }
274 try {303 try {
275 // Short path for JSONL files304 // Short path for JSONL files
276 if (request.body.format == 'jsonl') {305 if (request.body.format === 'jsonl') {
277 try {306 try {
278 const rawFile = fs.readFileSync(filename, 'utf8');307 const rawFile = fs.readFileSync(filename, 'utf8');
279 const successMessage = {308 const successMessage = {
@@ -283,8 +312,7 @@ router.post('/export', jsonParser, async function (request, response) {
283312
284 console.log(`Chat exported as ${exportfilename}`);313 console.log(`Chat exported as ${exportfilename}`);
285 return response.status(200).json(successMessage);314 return response.status(200).json(successMessage);
286 }315 } catch (err) {
287 catch (err) {
288 console.error(err);316 console.error(err);
289 const errorMessage = {317 const errorMessage = {
290 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,318 message: `Could not read JSONL file to export. Source chat file: ${filename}.`,
@@ -319,8 +347,7 @@ router.post('/export', jsonParser, async function (request, response) {
319 console.log(`Chat exported as ${exportfilename}`);347 console.log(`Chat exported as ${exportfilename}`);
320 return response.status(200).json(successMessage);348 return response.status(200).json(successMessage);
321 });349 });
322 }350 } catch (err) {
323 catch (err) {
324 console.log('chat export failed.');351 console.log('chat export failed.');
325 console.log(err);352 console.log(err);
326 return response.sendStatus(400);353 return response.sendStatus(400);
@@ -396,20 +423,36 @@ router.post('/import', urlencodedParser, function (request, response) {
396 }423 }
397424
398 if (format === 'jsonl') {425 if (format === 'jsonl') {
399 const line = data.split('\n')[0];426 let lines = data.split('\n');
427 const header = lines[0];
400428
401 const jsonData = JSON.parse(line);429 const jsonData = JSON.parse(header);
402430
403 if (jsonData.user_name !== undefined || jsonData.name !== undefined) {431 if (!(jsonData.user_name !== undefined || jsonData.name !== undefined)) {
404 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
405 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
406 fs.copyFileSync(pathToUpload, filePath);
407 fs.unlinkSync(pathToUpload);
408 response.send({ res: true });
409 } else {
410 console.log('Incorrect chat format .jsonl');432 console.log('Incorrect chat format .jsonl');
411 return response.send({ error: true });433 return response.send({ error: true });
412 }434 }
435
436 // Do a tiny bit of work to import Chub Chat data
437 // Processing the entire file is so fast that it's not worth checking if it's a Chub chat first
438 let flattenedChat;
439 try {
440 // flattening is unlikely to break, but it's not worth failing to
441 // import normal chats in an attempt to import a Chub chat
442 flattenedChat = flattenChubChat(userName, characterName, lines);
443 } catch (error) {
444 console.warn('Failed to flatten Chub Chat data: ', error);
445 }
446
447 const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
448 const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
449 if (flattenedChat !== data) {
450 writeFileAtomicSync(filePath, flattenedChat, 'utf8');
451 } else {
452 fs.copyFileSync(pathToUpload, filePath);
453 }
454 fs.unlinkSync(pathToUpload);
455 response.send({ res: true });
413 }456 }
414 } catch (error) {457 } catch (error) {
415 console.error(error);458 console.error(error);