Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Long conversations render only the area near the screen while keeping the full h

## Save or continue a conversation

“Save a copy…” starts with readable text. Other options appear when you choose a different purpose.
“Export conversation” starts with readable text. Other options appear when you choose a different purpose.

<p align="center">
<img src="img/en/export.png" alt="English save dialog, defaulting to reading and Save text" width="360">
Expand Down
2 changes: 2 additions & 0 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ New messages contain their entire Unicode and multiline text immediately in one
The custom conversation scrollbar is invisible at rest and absent without overflow. Tests observe opacity 1 during scrolling, an intermediate opacity during the 600 ms fade, and 0 after it finishes. They exercise actual pointer dragging, Home/End keyboard navigation and ARIA position updates. The browser retains native wheel and touch scrolling. Chinese and English UI labels, purpose-based export actions, bubble colors and matching documentation screenshots are checked. No added fading or backup-import menu items are present.

Browser integration was rerun with 100, 3,000 and 10,000 records after this change, including save/reload, complete exports, deletion/undo, offline reopening and narrow/landscape layouts. The 10,000-record case kept 10 live message rows. Opening timings above describe the earlier baseline comparison, not a new cross-device benchmark.

The compact menu is tested in Chinese and English at 278, 320 and 640 px, including increased font size. It follows the longest label, keeps its right padding within 18 px and stays inside the viewport without clipped text. The export action has no ellipsis. The updated SVG regenerates every web icon and the README logo; the PWA cache version changes to refresh these assets without touching conversation storage.
Binary file modified icons/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified icons/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified icons/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified icons/icon-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified icons/icon-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion icons/mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified icons/maskable-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/zh/dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@
visibility: hidden;
transform: translateY(-10px);
transition: opacity .2s ease-out, transform .2s ease-out;
width: 220px;
width: max-content;
max-width: calc(100% - 32px);
}

.menu-dropdown.show {
Expand Down Expand Up @@ -530,6 +531,9 @@
-webkit-user-select: none;
}

.menu-item > .icon { flex: 0 0 24px; text-align: center; }
.menu-item > span:last-child { min-width: 0; overflow-wrap: anywhere; }

.menu-item:hover {
background-color: var(--dropdown-hover);
}
Expand Down Expand Up @@ -897,8 +901,8 @@
}

.menu-dropdown {
width: 200px;
right: 10px;
max-width: calc(100% - 20px);
}

/* Mobile delete button adjustments */
Expand Down Expand Up @@ -1066,7 +1070,7 @@
</div>
<div class="menu-item" id="exportItem" role="menuitem" tabindex="-1">
<span class="icon">↓</span>
<span>Save a copy…</span>
<span>Export conversation</span>
</div>
<div class="menu-item" id="clearItem" role="menuitem" tabindex="-1">
<span class="icon">🗑️</span>
Expand Down Expand Up @@ -1256,7 +1260,7 @@
const translations={
'Say something…':'说点什么…','Type your message':'输入对话','Send message':'发送','Conversation':'对话',
'Open menu':'打开菜单','Menu options':'菜单','Change font size':'调整字号','Toggle dark mode':'深色外观','Dark appearance':'深色外观','Light appearance':'浅色外观',
'Save a copy…':'导出对话…','Clear conversation':'清空对话','Install app':'添加到主屏幕',
'Export conversation':'导出对话','Clear conversation':'清空对话','Install app':'添加到主屏幕',
'Install as App':'添加到主屏幕','Add this app to your home screen for a better experience':'从主屏幕打开对话。','Not Now':'稍后','Install':'添加',
'Opening conversation…':'正在打开对话…','Save conversation':'导出对话','Close save dialog':'关闭导出窗口',
'Save for':'用途','Reading':'阅读','Backup':'备份','Continuing with AI':'交给 AI 继续对话','Fine-tuning a model':'微调模型','Other formats':'其他格式',
Expand Down Expand Up @@ -1572,7 +1576,7 @@
let currentFontSize = CWGPreferences.getItem('fontSize') || 'normal';
let nightModeChecker, agingTimer; // Maintenance runs only while visible
let deferredPrompt; // Store the install prompt event
const APP_VERSION = '2.1.0'; // Version for cache management
const APP_VERSION = '2.1.1'; // Version for cache management

let repository, persistence = Promise.resolve(), storageFailed = false, readFailed = false;
let exporting = false;
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-icons.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ let sharp;try{sharp=require('sharp')}catch{if(!process.env.SHARP_MODULE)throw ne
(async()=>{
const svg=fs.readFileSync('icons/mark.svg');
for(const [filename,size] of Object.entries({'favicon-16x16.png':16,'favicon-32x32.png':32,'apple-touch-icon.png':180,'icon-192x192.png':192,'icon-512x512.png':512}))await sharp(svg).resize(size).png().toFile('icons/'+filename);
const mask=svg.toString().replace('<rect x="52"','<rect width="1024" height="1024" fill="#f8f5f1"/><rect x="52"');
const mask=svg.toString().replace(/<rect data-background="paper"[^>]+\/>/,'<rect width="1024" height="1024" fill="url(#paper)"/>');
await sharp(Buffer.from(mask)).resize(512).png().toFile('icons/maskable-icon.png');
await sharp(svg).resize(1024).png().toFile('img/logo.png');
})().catch(e=>{console.error(e);process.exitCode=1});
2 changes: 1 addition & 1 deletion service-worker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Cache only this application's public assets; conversations remain in IndexedDB.
const PREFIX = 'cwg:' + self.registration.scope + ':';
const CACHE_NAME = PREFIX + '2.1.0';
const CACHE_NAME = PREFIX + '2.1.1';
const ASSETS = ['./','./index.html','./manifest.json','./icons/favicon-32x32.png','./icons/apple-touch-icon.png','./icons/icon-192x192.png','./icons/icon-512x512.png','./icons/maskable-icon.png'];
self.addEventListener('install',event=>event.waitUntil(caches.open(CACHE_NAME).then(cache=>cache.addAll(ASSETS)).then(()=>self.skipWaiting())));
self.addEventListener('activate',event=>event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(key=>key.startsWith(PREFIX)&&key!==CACHE_NAME).map(key=>caches.delete(key)))).then(()=>self.clients.claim())));
Expand Down
17 changes: 16 additions & 1 deletion tests/motion.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ const root=path.resolve(__dirname,'..');
await context.close();console.log('Overlay visibility, gradual fade, dragging, keyboard and palette passed.');
}
{const {context,page}=await open();assert.equal(await page.locator('.scroll-indicator').isVisible(),false);await context.close();}
{const {context,page}=await open(2,{locale:'zh-CN'});assert.equal(await page.locator('html').getAttribute('lang'),'zh-CN');assert.equal(await page.locator('#messageInput').getAttribute('placeholder'),'说点什么…');await page.locator('#menuButton').click();assert.match(await page.locator('#exportItem').textContent(),/导出对话…/);await page.locator('#exportItem').click();await page.locator('#confirmExport').waitFor({state:'visible'});assert.equal(await page.locator('#confirmExport').innerText(),'保存文本');await page.locator('#exportPurpose').selectOption('training');assert.equal(await page.locator('#confirmExport').innerText(),'保存训练文件');assert.match(await page.locator('#exportSummary').innerText(),/训练样本/);await context.close();console.log('Actual Chinese interface and export labels passed.');}
for(const locale of ['zh-CN','en-US'])for(const width of [278,320,640]){
const {context,page}=await open(2,{locale,viewport:{width,height:568}});
await page.locator('#menuButton').click();await page.waitForTimeout(230);
const metrics=await page.locator('#menuDropdown').evaluate(menu=>{
const rows=[...menu.querySelectorAll('.menu-item')].filter(row=>getComputedStyle(row).display!=='none');
const texts=rows.map(row=>{const el=row.lastElementChild,range=document.createRange();range.selectNodeContents(el);return {right:range.getBoundingClientRect().right,width:range.getBoundingClientRect().width,overflow:el.scrollWidth>el.clientWidth};});
return {width:menu.offsetWidth,rightGap:menu.getBoundingClientRect().right-Math.max(...texts.map(t=>t.right)),overflow:texts.some(t=>t.overflow),inside:menu.getBoundingClientRect().left>=0&&menu.getBoundingClientRect().right<=innerWidth};
});
assert.ok(metrics.inside);assert.equal(metrics.overflow,false);assert.ok(metrics.rightGap<=18,'Menu follows the longest label, leaving only its padding');
if(locale==='zh-CN')assert.ok(metrics.width<180,'Chinese menu has no fixed empty right column');
await page.locator('#fontSizeItem').click();await page.locator('#menuButton').click();await page.waitForTimeout(230);
assert.ok(await page.locator('#menuDropdown').evaluate(el=>el.scrollWidth<=el.clientWidth&&el.getBoundingClientRect().left>=0));
await context.close();
}
console.log('Compact Chinese and English menus passed at narrow and wide sizes.');
{const {context,page}=await open(2,{locale:'zh-CN'});assert.equal(await page.locator('html').getAttribute('lang'),'zh-CN');assert.equal(await page.locator('#messageInput').getAttribute('placeholder'),'说点什么…');await page.locator('#menuButton').click();assert.equal((await page.locator('#exportItem > span:last-child').textContent()).trim(),'导出对话');await page.locator('#exportItem').click();await page.locator('#confirmExport').waitFor({state:'visible'});assert.equal(await page.locator('#confirmExport').innerText(),'保存文本');await page.locator('#exportPurpose').selectOption('training');assert.equal(await page.locator('#confirmExport').innerText(),'保存训练文件');assert.match(await page.locator('#exportSummary').innerText(),/训练样本/);await context.close();console.log('Actual Chinese interface and export labels passed.');}
}finally{await browser.close();}
})().catch(error=>{console.error(error);process.exitCode=1;});
2 changes: 1 addition & 1 deletion web/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const chinese=(navigator.language||'en').startsWith('zh');
const translations={
'Say something…':'说点什么…','Type your message':'输入对话','Send message':'发送','Conversation':'对话',
'Open menu':'打开菜单','Menu options':'菜单','Change font size':'调整字号','Toggle dark mode':'深色外观','Dark appearance':'深色外观','Light appearance':'浅色外观',
'Save a copy…':'导出对话…','Clear conversation':'清空对话','Install app':'添加到主屏幕',
'Export conversation':'导出对话','Clear conversation':'清空对话','Install app':'添加到主屏幕',
'Install as App':'添加到主屏幕','Add this app to your home screen for a better experience':'从主屏幕打开对话。','Not Now':'稍后','Install':'添加',
'Opening conversation…':'正在打开对话…','Save conversation':'导出对话','Close save dialog':'关闭导出窗口',
'Save for':'用途','Reading':'阅读','Backup':'备份','Continuing with AI':'交给 AI 继续对话','Fine-tuning a model':'微调模型','Other formats':'其他格式',
Expand Down
12 changes: 8 additions & 4 deletions web/index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@
visibility: hidden;
transform: translateY(-10px);
transition: opacity .2s ease-out, transform .2s ease-out;
width: 220px;
width: max-content;
max-width: calc(100% - 32px);
}

.menu-dropdown.show {
Expand Down Expand Up @@ -530,6 +531,9 @@
-webkit-user-select: none;
}

.menu-item > .icon { flex: 0 0 24px; text-align: center; }
.menu-item > span:last-child { min-width: 0; overflow-wrap: anywhere; }

.menu-item:hover {
background-color: var(--dropdown-hover);
}
Expand Down Expand Up @@ -897,8 +901,8 @@
}

.menu-dropdown {
width: 200px;
right: 10px;
max-width: calc(100% - 20px);
}

/* Mobile delete button adjustments */
Expand Down Expand Up @@ -1066,7 +1070,7 @@
</div>
<div class="menu-item" id="exportItem" role="menuitem" tabindex="-1">
<span class="icon">↓</span>
<span>Save a copy…</span>
<span>Export conversation</span>
</div>
<div class="menu-item" id="clearItem" role="menuitem" tabindex="-1">
<span class="icon">🗑️</span>
Expand Down Expand Up @@ -1294,7 +1298,7 @@
let currentFontSize = CWGPreferences.getItem('fontSize') || 'normal';
let nightModeChecker, agingTimer; // Maintenance runs only while visible
let deferredPrompt; // Store the install prompt event
const APP_VERSION = '2.1.0'; // Version for cache management
const APP_VERSION = '2.1.1'; // Version for cache management

let repository, persistence = Promise.resolve(), storageFailed = false, readFailed = false;
let exporting = false;
Expand Down
Loading