@@ -31,6 +31,34 @@ const markdownPreview = new MarkdownPreview();
3131const playground = new Playground ( ) ;
3232let playgroundActive = false ;
3333
34+ function showToast ( msg ) {
35+ let el = document . getElementById ( 'global-toast' ) ;
36+ if ( ! el ) {
37+ el = document . createElement ( 'div' ) ;
38+ el . id = 'global-toast' ;
39+ el . className = 'toast' ;
40+ document . body . appendChild ( el ) ;
41+ }
42+ el . textContent = msg ;
43+ el . classList . remove ( 'toast-hide' ) ;
44+ el . classList . add ( 'toast-show' ) ;
45+ clearTimeout ( el . _timer ) ;
46+ el . _timer = setTimeout ( ( ) => {
47+ el . classList . remove ( 'toast-show' ) ;
48+ el . classList . add ( 'toast-hide' ) ;
49+ } , 1500 ) ;
50+ }
51+
52+ function fallbackCopy ( text ) {
53+ const ta = document . createElement ( 'textarea' ) ;
54+ ta . value = text ;
55+ ta . style . cssText = 'position:fixed;opacity:0' ;
56+ document . body . appendChild ( ta ) ;
57+ ta . select ( ) ;
58+ document . execCommand ( 'copy' ) ;
59+ document . body . removeChild ( ta ) ;
60+ }
61+
3462// ── File opening ────────────────────────────────────────
3563
3664async function openFile ( path ) {
@@ -64,6 +92,17 @@ async function openFile(path) {
6492 renderEntryList ( ) ;
6593 updateStatusBar ( ) ;
6694
95+ document . getElementById ( 'status-errors' ) . textContent = '' ;
96+ invoke ( 'get_file_stats' ) . then ( ( stats ) => {
97+ const errEl = document . getElementById ( 'status-errors' ) ;
98+ if ( stats . parse_errors > 0 ) {
99+ errEl . textContent = `${ stats . parse_errors } 解析错误` ;
100+ errEl . style . color = 'var(--red)' ;
101+ } else {
102+ errEl . textContent = '' ;
103+ }
104+ } ) . catch ( ( ) => { } ) ;
105+
67106 if ( state . entries . length > 0 ) {
68107 selectEntry ( 0 ) ;
69108 } else {
@@ -132,19 +171,18 @@ async function selectEntry(index) {
132171 totalEntries : state . entries . length ,
133172 } ) ;
134173 } catch ( err ) {
135- console . error (
136- 'Failed to load entry:' ,
137- err ,
138- 'offset:' ,
139- entry . byte_offset ,
140- 'length:' ,
141- entry . byte_length ,
142- ) ;
174+ console . error ( 'Failed to load entry:' , err , 'offset:' , entry . byte_offset , 'length:' , entry . byte_length ) ;
143175 const viewEl = activeTab && document . getElementById ( `view-${ activeTab } ` ) ;
144176 if ( viewEl ) {
145- viewEl . innerHTML = `<div style="padding:20px;color:#ef4444;font-size:13px;">
146- <div style="font-weight:600;margin-bottom:4px;">加载条目失败</div>
147- <div style="font-size:12px;color:#fca5a5;">${ escapeHtml ( String ( err ) ) } </div>
177+ const isParseError = entry . task_id === '[PARSE ERROR]' ;
178+ const rawJson = isParseError ? await invoke ( 'get_entry' , {
179+ path : state . filePath , offset : entry . byte_offset , length : entry . byte_length ,
180+ } ) . catch ( ( ) => '' ) : '' ;
181+ viewEl . innerHTML = `<div class="entry-error-panel">
182+ <div class="entry-error-title">${ isParseError ? 'JSON 解析错误' : '加载条目失败' } </div>
183+ <div class="entry-error-msg">${ escapeHtml ( String ( err ) ) } </div>
184+ ${ rawJson ? `<div class="entry-error-label">原始内容(第 ${ entry . line_number } 行)</div>
185+ <pre class="entry-error-raw">${ escapeHtml ( rawJson ) } </pre>` : '' }
148186 </div>` ;
149187 }
150188 }
@@ -420,6 +458,7 @@ function initSettings() {
420458 const btnSettings = document . getElementById ( 'btn-settings' ) ;
421459 const btnSave = document . getElementById ( 'btn-save-settings' ) ;
422460 const btnCancel = document . getElementById ( 'btn-cancel-settings' ) ;
461+ const btnReset = document . getElementById ( 'btn-reset-settings' ) ;
423462 const formatRadios = document . querySelectorAll ( 'input[name="format"]' ) ;
424463 const chatFields = document . querySelectorAll ( '.field-chat' ) ;
425464 const promptFields = document . querySelectorAll ( '.field-prompt' ) ;
@@ -458,6 +497,18 @@ function initSettings() {
458497
459498 btnCancel . addEventListener ( 'click' , restoreAndClose ) ;
460499
500+ function applyDefaults ( ) {
501+ const d = DEFAULT_MAPPING ;
502+ document . querySelector ( `input[name="format"][value="${ d . format } "]` ) . checked = true ;
503+ document . getElementById ( 'set-task-id' ) . value = d . task_id_field ;
504+ document . getElementById ( 'set-messages' ) . value = d . messages_field ;
505+ document . getElementById ( 'set-prompt' ) . value = d . prompt_field ;
506+ document . getElementById ( 'set-result' ) . value = d . result_field ;
507+ showFields ( d . format ) ;
508+ }
509+
510+ btnReset . addEventListener ( 'click' , applyDefaults ) ;
511+
461512 modal . addEventListener ( 'click' , ( e ) => {
462513 if ( e . target === modal ) restoreAndClose ( ) ;
463514 } ) ;
@@ -575,19 +626,20 @@ function initPlayground() {
575626 const btn = document . getElementById ( 'btn-playground' ) ;
576627 const panel = document . getElementById ( 'playground-panel' ) ;
577628 const mainContent = document . getElementById ( 'main-content' ) ;
629+ let rendered = false ;
578630
579631 btn . addEventListener ( 'click' , ( ) => {
580632 playgroundActive = ! playgroundActive ;
581633 if ( playgroundActive ) {
582634 btn . classList . add ( 'active' ) ;
583635 mainContent . classList . add ( 'hidden' ) ;
584636 panel . classList . remove ( 'hidden' ) ;
585- // Defer render until after the browser has recalculated layout
586- // (panel was display:none, needs a frame to get correct dimensions)
587- requestAnimationFrame ( ( ) => playground . render ( panel ) ) ;
637+ if ( ! rendered ) {
638+ requestAnimationFrame ( ( ) => playground . render ( panel ) ) ;
639+ rendered = true ;
640+ }
588641 } else {
589642 btn . classList . remove ( 'active' ) ;
590- playground . destroy ( ) ;
591643 panel . classList . add ( 'hidden' ) ;
592644 mainContent . classList . remove ( 'hidden' ) ;
593645 }
@@ -612,6 +664,25 @@ function init() {
612664
613665 document . getElementById ( 'btn-open' ) . addEventListener ( 'click' , ( ) => openFile ( ) ) ;
614666 document . getElementById ( 'btn-welcome-open' ) . addEventListener ( 'click' , ( ) => openFile ( ) ) ;
667+ document . getElementById ( 'btn-copy-entry' ) . addEventListener ( 'click' , ( e ) => {
668+ if ( ! state . currentEntry ) return ;
669+ const text = JSON . stringify ( state . currentEntry , null , 2 ) ;
670+ const btn = e . currentTarget ;
671+ const onSuccess = ( ) => {
672+ btn . classList . add ( 'copied' ) ;
673+ setTimeout ( ( ) => btn . classList . remove ( 'copied' ) , 1500 ) ;
674+ showToast ( '已复制到剪贴板' ) ;
675+ } ;
676+ if ( navigator . clipboard ?. writeText ) {
677+ navigator . clipboard . writeText ( text ) . then ( onSuccess ) . catch ( ( ) => {
678+ fallbackCopy ( text ) ;
679+ onSuccess ( ) ;
680+ } ) ;
681+ } else {
682+ fallbackCopy ( text ) ;
683+ onSuccess ( ) ;
684+ }
685+ } ) ;
615686 document . getElementById ( 'btn-prev' ) . addEventListener ( 'click' , ( ) => {
616687 if ( state . currentIndex > 0 ) selectEntry ( state . currentIndex - 1 ) ;
617688 } ) ;
0 commit comments