@@ -32,6 +32,8 @@ import type {
3232import { EuphonySearchWindow } from '../search-window/search-window' ;
3333import { NightjarToast } from '../toast/toast' ;
3434import { EuphonyTokenWindow } from '../token-window/token-window' ;
35+ import type { LocalDataWorkerMessage } from './local-data-worker' ;
36+ import LocalDataWorkerInline from './local-data-worker?worker' ;
3537import { RequestWorker } from './request-worker' ;
3638import { URLManager } from './url-manager' ;
3739
@@ -276,6 +278,19 @@ export class EuphonyApp extends LitElement {
276278
277279 // URL manager
278280 urlManager : URLManager ;
281+ localDataWorker : Worker ;
282+ localDataWorkerRequestCount = 0 ;
283+ get localDataWorkerRequestID ( ) {
284+ return this . localDataWorkerRequestCount ++ ;
285+ }
286+ activeLocalDataWorkerRequestID : number | null = null ;
287+ localDataWorkerPendingRequests = new Map <
288+ number ,
289+ {
290+ resolve : ( ) => void ;
291+ reject : ( reason ?: unknown ) => void ;
292+ }
293+ > ( ) ;
279294
280295 // Debouncers
281296 cacheInfoTooltipDebouncer : number | null = null ;
@@ -287,6 +302,13 @@ export class EuphonyApp extends LitElement {
287302 super ( ) ;
288303
289304 this . urlManager = new URLManager ( this ) ;
305+ this . localDataWorker = new LocalDataWorkerInline ( ) ;
306+ this . localDataWorker . addEventListener (
307+ 'message' ,
308+ ( e : MessageEvent < LocalDataWorkerMessage > ) => {
309+ this . localDataWorkerMessageHandler ( e ) ;
310+ }
311+ ) ;
290312
291313 // Update the configs based on the current URL
292314 this . urlManager . updateConfigsFromURL ( ) ;
@@ -380,6 +402,11 @@ export class EuphonyApp extends LitElement {
380402 } ) ;
381403 }
382404
405+ disconnectedCallback ( ) : void {
406+ this . localDataWorker . terminate ( ) ;
407+ super . disconnectedCallback ( ) ;
408+ }
409+
383410 /**
384411 * This method is called when the DOM is added for the first time
385412 */
@@ -786,12 +813,14 @@ export class EuphonyApp extends LitElement {
786813 break ;
787814 }
788815 case 'Load from clipboard' : {
816+ this . isLoadingData = true ;
789817 navigator . clipboard . readText ( ) . then (
790- clipText => {
791- this . loadDataFromText ( clipText , 'clipboard' ) ;
818+ async clipText => {
819+ await this . loadDataFromText ( clipText , 'clipboard' ) ;
792820 } ,
793821 ( err : unknown ) => {
794822 console . error ( 'Failed to read clipboard contents: ' , err ) ;
823+ this . isLoadingData = false ;
795824 }
796825 ) ;
797826 break ;
@@ -1300,125 +1329,147 @@ export class EuphonyApp extends LitElement {
13001329 }
13011330 } ;
13021331
1303- loadDataFromText = ( sourceText : string , sourceName : 'clipboard' | 'file' ) => {
1304- let allData : ( Record < string , unknown > | string | Conversation ) [ ] = [ ] ;
1305- // Try to convert the source text to a JSON or JSONL file.
1306- try {
1307- const jsonData = JSON . parse ( sourceText ) as Record < string , unknown > ;
1308- allData = [ jsonData ] ;
1309- } catch ( _error ) {
1310- // Try to read each line as a JSON object
1311- for ( const line of sourceText . split ( '\n' ) ) {
1312- try {
1313- allData . push ( JSON . parse ( line ) as Record < string , unknown > | string ) ;
1314- } catch ( _error ) {
1315- // pass
1332+ loadDataFromText = (
1333+ sourceText : string ,
1334+ sourceName : 'clipboard' | 'file'
1335+ ) => {
1336+ this . curPage = 1 ;
1337+ this . resetHash ( ) ;
1338+ const requestID = this . localDataWorkerRequestID ;
1339+ this . activeLocalDataWorkerRequestID = requestID ;
1340+
1341+ return new Promise < void > ( ( resolve , reject ) => {
1342+ this . localDataWorkerPendingRequests . set ( requestID , { resolve, reject } ) ;
1343+ const message : LocalDataWorkerMessage = {
1344+ command : 'startParseData' ,
1345+ payload : {
1346+ requestID,
1347+ sourceName,
1348+ sourceText
13161349 }
1317- }
1318- }
1350+ } ;
1351+ this . localDataWorker . postMessage ( message ) ;
1352+ } ) ;
1353+ } ;
13191354
1320- // Return if there is no data read
1321- if ( allData . length === 0 ) {
1322- this . toastMessage = `Failed to read any JSON or JSONL data from your ${ sourceName } . Please double check and try again.` ;
1323- this . toastType = 'error' ;
1324- if ( this . toastComponent ) {
1325- this . toastComponent . show ( ) ;
1326- }
1327- return ;
1328- }
1355+ loadDataFromFile = ( sourceFile : File ) => {
1356+ this . curPage = 1 ;
1357+ this . resetHash ( ) ;
1358+ const requestID = this . localDataWorkerRequestID ;
1359+ this . activeLocalDataWorkerRequestID = requestID ;
1360+
1361+ return new Promise < void > ( ( resolve , reject ) => {
1362+ this . localDataWorkerPendingRequests . set ( requestID , { resolve, reject } ) ;
1363+ const message : LocalDataWorkerMessage = {
1364+ command : 'startParseData' ,
1365+ payload : {
1366+ requestID,
1367+ sourceName : 'file' ,
1368+ sourceFile
1369+ }
1370+ } ;
1371+ this . localDataWorker . postMessage ( message ) ;
1372+ } ) ;
1373+ } ;
13291374
1330- this . codexSessionData = [ ] ;
1375+ localDataWorkerMessageHandler ( e : MessageEvent < LocalDataWorkerMessage > ) {
1376+ switch ( e . data . command ) {
1377+ case 'finishParseData' : {
1378+ const { requestID, sourceName, dataType } = e . data . payload ;
1379+ const pendingRequest =
1380+ this . localDataWorkerPendingRequests . get ( requestID ) ;
1381+ this . localDataWorkerPendingRequests . delete ( requestID ) ;
1382+ if ( requestID !== this . activeLocalDataWorkerRequestID ) {
1383+ pendingRequest ?. resolve ( ) ;
1384+ break ;
1385+ }
1386+ blobPath = null ;
1387+ this . isLoadingData = false ;
13311388
1332- // Codex session JSONL is a stream of event objects, not Harmony
1333- // conversations. Detect it early and render with the Codex component.
1334- if ( isCodexSessionJSONL ( allData as unknown [ ] ) ) {
1335- this . codexSessionData = [ allData as unknown [ ] ] ;
1336- this . allConversationData = [ ] ;
1337- this . conversationData = [ ] ;
1338- this . JSONData = [ ] ;
1339- this . selectedConversationIDs = new Set ( ) ;
1340- this . dataType = DataType . CODEX ;
1341- this . _totalConversationSize = 1 ;
1342- this . _totalConversationSizeIncludingUnfiltered = 1 ;
1343- this . isLoadingFromCache = false ;
1344- this . isLoadingFromClipboard = true ;
1345-
1346- this . toastMessage = `Codex session loaded successfully from ${ sourceName } ` ;
1347- this . toastType = 'success' ;
1348- if ( this . toastComponent ) {
1349- this . toastComponent . show ( ) ;
1350- }
1351- return ;
1352- }
1389+ this . codexSessionData = [ ] ;
1390+ this . allConversationData = [ ] ;
1391+ this . conversationData = [ ] ;
1392+ this . JSONData = [ ] ;
13531393
1354- // Validate the data
1355- // If the data is not a conversation, we render it as JSON
1356- if ( ! this . validateAndTransformConversations ( allData ) ) {
1357- this . toastMessage =
1358- 'Failed to find harmony-formatted data. Render JSON instead.' ;
1359- this . toastType = 'warning' ;
1360- if ( this . toastComponent ) {
1361- this . toastComponent . show ( ) ;
1362- }
1394+ if ( dataType === 'codex' ) {
1395+ this . codexSessionData = [ e . data . payload . codexSessionData ] ;
1396+ this . selectedConversationIDs = new Set ( ) ;
1397+ this . dataType = DataType . CODEX ;
1398+ this . _totalConversationSize = 1 ;
1399+ this . _totalConversationSizeIncludingUnfiltered = 1 ;
1400+ this . isLoadingFromCache = false ;
1401+ this . isLoadingFromClipboard = true ;
13631402
1364- this . JSONData = allData as Record < string , unknown > [ ] ;
1365- this . dataType = DataType . JSON ;
1366- this . _totalConversationSize = allData . length ;
1367- this . _totalConversationSizeIncludingUnfiltered = allData . length ;
1368- return ;
1369- }
1403+ this . toastMessage = `Codex session loaded successfully from ${ sourceName } ` ;
1404+ this . toastType = 'success' ;
1405+ } else if ( dataType === 'json' ) {
1406+ this . JSONData = e . data . payload . jsonData ;
1407+ this . dataType = DataType . JSON ;
1408+ this . _totalConversationSize = this . JSONData . length ;
1409+ this . _totalConversationSizeIncludingUnfiltered = this . JSONData . length ;
1410+ this . isLoadingFromCache = false ;
1411+ this . isLoadingFromClipboard = true ;
1412+
1413+ this . toastMessage =
1414+ 'Failed to find harmony-formatted data. Render JSON instead.' ;
1415+ this . toastType = 'warning' ;
1416+ } else {
1417+ const conversationData = e . data . payload . conversationData ;
1418+ this . _totalConversationSize = conversationData . length ;
1419+ this . _totalConversationSizeIncludingUnfiltered =
1420+ conversationData . length ;
1421+
1422+ if ( this . isEditorMode ) {
1423+ this . selectedConversationIDs = new Set ( ) ;
1424+ for ( let i = 0 ; i < conversationData . length ; i ++ ) {
1425+ this . selectedConversationIDs . add ( i ) ;
1426+ }
1427+ }
1428+
1429+ this . allConversationData = conversationData ;
1430+ this . conversationData = this . isEditorMode
1431+ ? conversationData
1432+ : conversationData . slice (
1433+ ( this . curPage - 1 ) * this . itemsPerPage ,
1434+ this . curPage * this . itemsPerPage
1435+ ) ;
1436+ this . dataType = DataType . CONVERSATION ;
1437+ this . isLoadingFromCache = false ;
1438+ this . isLoadingFromClipboard = true ;
13701439
1371- // The data is valid conversation, so we render it as conversations
1372- this . _totalConversationSize = allData . length ;
1373- this . _totalConversationSizeIncludingUnfiltered = allData . length ;
1440+ this . toastMessage = `Data loaded successfully from ${ sourceName } ` ;
1441+ this . toastType = 'success' ;
1442+ }
13741443
1375- // Set all the conversations as selected in editor mode
1376- if ( this . isEditorMode ) {
1377- this . selectedConversationIDs = new Set ( ) ;
1378- for ( let i = 0 ; i < allData . length ; i ++ ) {
1379- this . selectedConversationIDs . add ( i ) ;
1444+ this . toastComponent ?. show ( ) ;
1445+ pendingRequest ?. resolve ( ) ;
1446+ break ;
13801447 }
1381- }
13821448
1383- // People might encode the JSON differently, so we need to load them based
1384- // on the type
1385- if ( typeof allData [ 0 ] === 'string' ) {
1386- const newData : Conversation [ ] = allData . map ( item => {
1387- if ( typeof item === 'string' ) {
1388- const parsed = parseConversationJSONString ( item ) ;
1389- if ( parsed === null ) {
1390- this . toastMessage = `Failed to format JSONL data from your ${ sourceName } . Please double check and try again.` ;
1391- this . toastType = 'error' ;
1392- if ( this . toastComponent ) {
1393- this . toastComponent . show ( ) ;
1394- }
1395- throw new Error ( 'Failed to parse conversation JSON string' ) ;
1396- }
1397- return parsed ;
1449+ case 'error' : {
1450+ const { requestID, sourceName, message } = e . data . payload ;
1451+ const pendingRequest =
1452+ this . localDataWorkerPendingRequests . get ( requestID ) ;
1453+ this . localDataWorkerPendingRequests . delete ( requestID ) ;
1454+ if ( requestID !== this . activeLocalDataWorkerRequestID ) {
1455+ pendingRequest ?. reject ( new Error ( message ) ) ;
1456+ break ;
13981457 }
1399- return item as Conversation ;
1400- } ) ;
1401- this . allConversationData = newData ;
1402- this . conversationData = newData ;
1403- this . dataType = DataType . CONVERSATION ;
1404- } else {
1405- const typedData = allData as Conversation [ ] ;
1406- this . allConversationData = typedData ;
1407- this . conversationData = typedData ;
1408- this . dataType = DataType . CONVERSATION ;
1409- }
1458+ this . isLoadingData = false ;
14101459
1411- // Update the cache info
1412- this . isLoadingFromCache = false ;
1413- this . isLoadingFromClipboard = true ;
1460+ this . toastMessage = `Failed to read any JSON or JSONL data from your ${ sourceName } . Please double check and try again.\n\n${ message } ` ;
1461+ this . toastType = 'error' ;
1462+ this . toastComponent ?. show ( ) ;
1463+ pendingRequest ?. reject ( new Error ( message ) ) ;
1464+ break ;
1465+ }
14141466
1415- // Show a successful toast
1416- this . toastMessage = `Data loaded successfully from ${ sourceName } ` ;
1417- this . toastType = 'success' ;
1418- if ( this . toastComponent ) {
1419- this . toastComponent . show ( ) ;
1467+ default : {
1468+ console . error ( 'Unknown local data worker message' , e . data . command ) ;
1469+ break ;
1470+ }
14201471 }
1421- } ;
1472+ }
14221473
14231474 localFileInputChanged ( e : Event ) {
14241475 const inputElement = e . target as HTMLInputElement ;
@@ -1427,15 +1478,13 @@ export class EuphonyApp extends LitElement {
14271478 return ;
14281479 }
14291480
1430- file
1431- . text ( )
1432- . then ( text => {
1433- this . loadDataFromText ( text , 'file' ) ;
1434- } )
1481+ this . isLoadingData = true ;
1482+ this . loadDataFromFile ( file )
14351483 . catch ( ( error : unknown ) => {
14361484 this . toastMessage = `Failed to read local file.\n\n${ error } ` ;
14371485 this . toastType = 'error' ;
14381486 this . toastComponent ?. show ( ) ;
1487+ this . isLoadingData = false ;
14391488 } )
14401489 . finally ( ( ) => {
14411490 inputElement . value = '' ;
0 commit comments