Skip to content

Commit bd8bbf0

Browse files
committed
chore: 体验优化
1 parent f6b8bfe commit bd8bbf0

6 files changed

Lines changed: 254 additions & 21 deletions

File tree

index.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
</div>
5454
</div>
5555
<div class="toolbar-right">
56-
<button id="btn-playground" title="粘贴测试 JSON/Markdown">
56+
<button id="btn-playground" title="Playground — 粘贴测试 JSON/Markdown">
5757
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
5858
<rect
5959
x="2"
@@ -75,7 +75,7 @@
7575
</svg>
7676
<span>Playground</span>
7777
</button>
78-
<button id="btn-settings" title="设置 JSONL 字段映射关系">
78+
<button id="btn-settings" title="设置 JSONL 字段映射关系">
7979
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
8080
<circle cx="12" cy="12" r="3" />
8181
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
@@ -119,6 +119,12 @@
119119
<button class="tab-btn" data-tab="raw">原始</button>
120120
</div>
121121
<div class="panel-tabs-right">
122+
<button id="btn-copy-entry" class="tab-nav-btn" title="复制当前条目 JSON">
123+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
124+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
125+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
126+
</svg>
127+
</button>
122128
<button id="btn-prev" class="tab-nav-btn" title="上一条 (↑)" disabled>
123129
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
124130
<path
@@ -176,6 +182,7 @@
176182
<span id="status-size"></span>
177183
<span id="status-messages"></span>
178184
<span id="status-chars"></span>
185+
<span id="status-errors"></span>
179186
</footer>
180187

181188
<!-- Welcome screen overlay -->
@@ -267,6 +274,7 @@ <h3>字段映射设置</h3>
267274
</div>
268275

269276
<div class="modal-actions">
277+
<button id="btn-reset-settings" class="btn-text">恢复默认</button>
270278
<button id="btn-cancel-settings" class="btn-secondary">取消</button>
271279
<button id="btn-save-settings" class="btn-primary">保存</button>
272280
</div>

src/components/chat-view.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { escapeHtml } from '../utils/escape-html.js';
22

3+
function _fallbackCopy(text) {
4+
const ta = document.createElement('textarea');
5+
ta.value = text;
6+
ta.style.cssText = 'position:fixed;opacity:0';
7+
document.body.appendChild(ta);
8+
ta.select();
9+
document.execCommand('copy');
10+
document.body.removeChild(ta);
11+
}
12+
313
export class ChatView {
414
render(data, container) {
515
this._data = data;
@@ -41,7 +51,10 @@ export class ChatView {
4151
${reasoningHtml}
4252
<div class="chat-content" data-index="${index}" data-full="${isLong ? '1' : '0'}">${escapeHtml(displayContent)}${isLong ? '...' : ''}</div>
4353
${isLong ? `<button class="chat-toggle" data-index="${index}">显示全部 (${charCount} chars)</button>` : ''}
44-
<div class="chat-meta">${charCount} 字符 · ~${tokenEstimate} tokens</div>
54+
<div class="chat-message-footer">
55+
<div class="chat-meta">${charCount} 字符 · ~${tokenEstimate} tokens</div>
56+
<button class="chat-copy-btn" data-index="${index}">复制</button>
57+
</div>
4558
</div>`;
4659
}
4760

@@ -67,5 +80,27 @@ export class ChatView {
6780
}
6881
});
6982
});
83+
84+
container.querySelectorAll('.chat-copy-btn').forEach((btn) => {
85+
btn.addEventListener('click', () => {
86+
const index = btn.dataset.index;
87+
const messages = this._data?.messages;
88+
if (!messages?.[index]) return;
89+
const text = messages[index].content || '';
90+
const onDone = () => {
91+
btn.textContent = '已复制';
92+
setTimeout(() => { btn.textContent = '复制'; }, 1500);
93+
};
94+
if (navigator.clipboard?.writeText) {
95+
navigator.clipboard.writeText(text).then(onDone).catch(() => {
96+
_fallbackCopy(text);
97+
onDone();
98+
});
99+
} else {
100+
_fallbackCopy(text);
101+
onDone();
102+
}
103+
});
104+
});
70105
}
71106
}

src/components/markdown-preview.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export class MarkdownPreview {
128128
html += `<span class="html-block-spacer"></span>`;
129129
html += `<button class="html-nav-btn" data-action="next" title="下一条 (→)"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
130130
html += `<button data-action="copy" data-index="${i}">复制代码</button>`;
131+
html += `<button data-action="refresh" data-index="${i}">刷新</button>`;
131132
html += `<button data-action="fullscreen" data-index="${i}">全屏</button>`;
132133
html += `</div></div>`;
133134
html += `<div class="html-preview-slot" id="html-slot-${i}"></div>`;
@@ -286,6 +287,14 @@ export class MarkdownPreview {
286287
});
287288
break;
288289
}
290+
case 'refresh': {
291+
const slot = container.querySelector(`#html-slot-${index}`);
292+
if (slot) {
293+
slot.innerHTML = '';
294+
this._showIframePreview(container, block.code, index);
295+
}
296+
break;
297+
}
289298
case 'fullscreen': {
290299
const box = container.querySelector(`#html-container-${index}`);
291300
if (box) {

src/main.js

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,34 @@ const markdownPreview = new MarkdownPreview();
3131
const playground = new Playground();
3232
let 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

3664
async 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

Comments
 (0)