From 531e0beb96c877c7f89ac8e47e628aaa8093ac1d Mon Sep 17 00:00:00 2001
From: MoringstarsH <2671148074@qq.com>
Date: Thu, 28 May 2026 19:17:08 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=AD=E6=96=87?=
=?UTF-8?q?=E7=95=8C=E9=9D=A2=E5=88=87=E6=8D=A2=E5=92=8C=E7=99=BD=E5=A4=A9?=
=?UTF-8?q?=E6=A8=A1=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
static/channels.js | 36 +-
static/chat.js | 338 ++++++++--------
static/i18n.js | 877 ++++++++++++++++++++++++++++++++++++++++++
static/index.html | 173 +++++----
static/jobs.css | 19 +
static/jobs.js | 82 ++--
static/rules-panel.js | 40 +-
static/sessions.css | 4 +
static/sessions.js | 193 ++++++----
static/style.css | 122 +++++-
static/theme.js | 46 +++
11 files changed, 1540 insertions(+), 390 deletions(-)
create mode 100644 static/i18n.js
create mode 100644 static/theme.js
diff --git a/static/channels.js b/static/channels.js
index a74b7332..86b5f15b 100644
--- a/static/channels.js
+++ b/static/channels.js
@@ -63,14 +63,14 @@ function renderChannelTabs() {
const editBtn = document.createElement('button');
editBtn.className = 'ch-edit-btn';
- editBtn.title = 'Rename';
+ editBtn.title = t('channel.rename.title');
editBtn.innerHTML = '';
editBtn.onclick = (e) => { e.stopPropagation(); showChannelRenameDialog(name); };
actions.appendChild(editBtn);
const delBtn = document.createElement('button');
delBtn.className = 'ch-delete-btn';
- delBtn.title = 'Delete';
+ delBtn.title = t('channel.delete.title');
delBtn.innerHTML = '';
delBtn.onclick = (e) => { e.stopPropagation(); deleteChannel(name); };
actions.appendChild(delBtn);
@@ -142,13 +142,13 @@ function renderChannelSidebar() {
actions.className = 'channel-sidebar-row-actions';
const editBtn = document.createElement('button');
- editBtn.title = 'Rename';
+ editBtn.title = t('channel.rename.title');
editBtn.innerHTML = '';
editBtn.onclick = (e) => { e.stopPropagation(); _showSidebarRenameDialog(name); };
actions.appendChild(editBtn);
const delBtn = document.createElement('button');
- delBtn.title = 'Delete';
+ delBtn.title = t('channel.delete.title');
delBtn.innerHTML = '';
delBtn.onclick = (e) => { e.stopPropagation(); _sidebarConfirmDelete(name, row, label); };
actions.appendChild(delBtn);
@@ -198,7 +198,7 @@ function _showSidebarRenameDialog(oldName) {
const confirm = document.createElement('button');
confirm.className = 'confirm-btn';
confirm.innerHTML = '✓';
- confirm.title = 'Rename';
+ confirm.title = t('channel.rename.title');
confirm.onclick = () => {
const newName = input.value.trim().toLowerCase();
if (!newName || !/^[a-z0-9][a-z0-9\-]{0,19}$/.test(newName)) return;
@@ -217,7 +217,7 @@ function _showSidebarRenameDialog(oldName) {
const cancel = document.createElement('button');
cancel.className = 'cancel-btn';
cancel.innerHTML = '✕';
- cancel.title = 'Cancel';
+ cancel.title = t('common.cancel');
cancel.onclick = cleanup;
wrapper.appendChild(cancel);
@@ -245,7 +245,7 @@ function _sidebarConfirmDelete(name, row, label) {
const originalOnclick = row.onclick;
row.classList.add('confirm-delete');
- label.textContent = `delete #${name}?`;
+ label.textContent = t('channel.deleteConfirm', { name });
if (actions) actions.style.display = 'none';
const confirmBar = document.createElement('span');
@@ -253,11 +253,11 @@ function _sidebarConfirmDelete(name, row, label) {
confirmBar.style.display = 'flex';
const tickBtn = document.createElement('button');
- tickBtn.title = 'Confirm delete';
+ tickBtn.title = t('common.confirm');
tickBtn.innerHTML = '';
const crossBtn = document.createElement('button');
- crossBtn.title = 'Cancel';
+ crossBtn.title = t('common.cancel');
crossBtn.innerHTML = '';
confirmBar.appendChild(tickBtn);
@@ -360,14 +360,14 @@ function showChannelCreateDialog() {
const confirm = document.createElement('button');
confirm.className = 'confirm-btn';
confirm.innerHTML = '✓';
- confirm.title = 'Create';
+ confirm.title = t('common.create');
confirm.onclick = () => { _submitInlineCreate(input, wrapper); if (addBtn) addBtn.style.display = ''; };
wrapper.appendChild(confirm);
const cancel = document.createElement('button');
cancel.className = 'cancel-btn';
cancel.innerHTML = '✕';
- cancel.title = 'Cancel';
+ cancel.title = t('common.cancel');
cancel.onclick = cleanup;
wrapper.appendChild(cancel);
@@ -425,7 +425,7 @@ function showChannelRenameDialog(oldName) {
const confirm = document.createElement('button');
confirm.className = 'confirm-btn';
confirm.innerHTML = '✓';
- confirm.title = 'Rename';
+ confirm.title = t('channel.rename.title');
confirm.onclick = () => {
const newName = input.value.trim().toLowerCase();
if (!newName || !/^[a-z0-9][a-z0-9\-]{0,19}$/.test(newName)) return;
@@ -444,7 +444,7 @@ function showChannelRenameDialog(oldName) {
const cancel = document.createElement('button');
cancel.className = 'cancel-btn';
cancel.innerHTML = '✕';
- cancel.title = 'Cancel';
+ cancel.title = t('common.cancel');
cancel.onclick = cleanup;
wrapper.appendChild(cancel);
@@ -482,7 +482,7 @@ function deleteChannel(name) {
tab.classList.add('confirm-delete');
tab.classList.remove('editing');
- label.textContent = `delete #${name}?`;
+ label.textContent = t('channel.deleteConfirm', { name });
if (actions) actions.style.display = 'none';
const confirmBar = document.createElement('span');
@@ -490,12 +490,12 @@ function deleteChannel(name) {
const tickBtn = document.createElement('button');
tickBtn.className = 'ch-confirm-yes';
- tickBtn.title = 'Confirm delete';
+ tickBtn.title = t('common.confirm');
tickBtn.innerHTML = '';
const crossBtn = document.createElement('button');
crossBtn.className = 'ch-confirm-no';
- crossBtn.title = 'Cancel';
+ crossBtn.title = t('common.cancel');
crossBtn.innerHTML = '';
confirmBar.appendChild(tickBtn);
@@ -577,12 +577,12 @@ function _updateSupportLabel() {
if (!label) return;
const inSidebar = document.body.classList.contains('channels-in-sidebar');
if (!inSidebar) {
- label.textContent = ' Support development';
+ label.textContent = t('header.support');
return;
}
const panel = document.getElementById('channel-sidebar');
const w = panel ? panel.offsetWidth : 200;
- label.textContent = w < 200 ? ' Support' : ' Support development';
+ label.textContent = w < 200 ? t('header.support.short') : t('header.support');
}
function setupChannelSidebarGrip() {
diff --git a/static/chat.js b/static/chat.js
index 94c5c373..a9a89ff1 100644
--- a/static/chat.js
+++ b/static/chat.js
@@ -66,14 +66,14 @@ function enableDragScroll(el) {
// --- Notification sounds ---
const SOUND_OPTIONS = [
- { value: 'soft-chime', label: 'Soft Chime' },
- { value: 'bright-ping', label: 'Bright Ping' },
- { value: 'gentle-pop', label: 'Gentle Pop' },
- { value: 'alert-tone', label: 'Alert Tone' },
- { value: 'pluck', label: 'Pluck' },
- { value: 'click', label: 'Click' },
- { value: 'warm-bell', label: 'Warm Bell' },
- { value: 'none', label: 'None' },
+ { value: 'soft-chime', labelKey: 'sound.softChime' },
+ { value: 'bright-ping', labelKey: 'sound.brightPing' },
+ { value: 'gentle-pop', labelKey: 'sound.gentlePop' },
+ { value: 'alert-tone', labelKey: 'sound.alertTone' },
+ { value: 'pluck', labelKey: 'sound.pluck' },
+ { value: 'click', labelKey: 'sound.click' },
+ { value: 'warm-bell', labelKey: 'sound.warmBell' },
+ { value: 'none', labelKey: 'sound.none' },
];
const DEFAULT_SOUND = 'soft-chime';
const CROSS_CHANNEL_SOUND = 'pluck';
@@ -116,8 +116,8 @@ function buildSoundSettings() {
row.className = 'sound-row';
const label = document.createElement('span');
label.className = 'sound-label';
- label.textContent = name === 'default' ? 'Default sound'
- : name === 'cross-channel' ? 'Background alerts'
+ label.textContent = name === 'default' ? t('sound.default')
+ : name === 'cross-channel' ? t('sound.background')
: (agentConfig[name]?.label || name);
const select = document.createElement('select');
select.className = 'sound-select';
@@ -127,7 +127,7 @@ function buildSoundSettings() {
for (const opt of SOUND_OPTIONS) {
const o = document.createElement('option');
o.value = opt.value;
- o.textContent = opt.label;
+ o.textContent = t(opt.labelKey);
if (currentVal === opt.value) o.selected = true;
select.appendChild(o);
}
@@ -135,7 +135,7 @@ function buildSoundSettings() {
if (name !== 'default' && name !== 'cross-channel') {
const o = document.createElement('option');
o.value = '';
- o.textContent = 'Use default';
+ o.textContent = t('sound.useDefault');
if (!soundPrefs[name]) o.selected = true;
select.insertBefore(o, select.firstChild);
}
@@ -207,9 +207,9 @@ async function checkForUpdate() {
return;
}
- const label = data.state === 'upstream_update' ? 'Upstream update available' : 'Update available';
+ const label = data.state === 'upstream_update' ? t('update.upstream') : t('update.available');
pill.href = data.url || 'https://github.com/bcurts/agentchattr/releases';
- pill.innerHTML = `${label}`;
+ pill.innerHTML = `${label}`;
pill.classList.remove('hidden');
} catch {
// Silent fail -- version check should never block the UI
@@ -347,17 +347,17 @@ function addCodeCopyButtons(container) {
if (pre.querySelector('.code-copy-btn')) continue;
const btn = document.createElement('button');
btn.className = 'code-copy-btn';
- btn.textContent = 'copy';
+ btn.textContent = t('timeline.copyButton');
btn.onclick = async (e) => {
e.stopPropagation();
const code = pre.querySelector('code')?.textContent || pre.textContent;
try {
await navigator.clipboard.writeText(code);
- btn.textContent = 'copied!';
- setTimeout(() => { btn.textContent = 'copy'; }, 1500);
+ btn.textContent = t('timeline.copied');
+ setTimeout(() => { btn.textContent = t('timeline.copyButton'); }, 1500);
} catch (err) {
- btn.textContent = 'failed';
- setTimeout(() => { btn.textContent = 'copy'; }, 1500);
+ btn.textContent = t('timeline.failed');
+ setTimeout(() => { btn.textContent = t('timeline.copyButton'); }, 1500);
}
};
pre.style.position = 'relative';
@@ -393,7 +393,7 @@ function connectWebSocket() {
const choicesEl = existing.querySelector('.decision-choices');
const meta = updated.metadata || {};
if (choicesEl && meta.resolved) {
- choicesEl.innerHTML = `
You chose: ${escapeHtml(meta.chosen || '')}
`;
+ choicesEl.innerHTML = `${escapeHtml(t('timeline.youChose', { choice: meta.chosen || '' }))}
`;
}
}
}
@@ -633,8 +633,8 @@ function formatDateDivider(dateStr) {
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
- if (date.toDateString() === today.toDateString()) return 'Today';
- if (date.toDateString() === yesterday.toDateString()) return 'Yesterday';
+ if (date.toDateString() === today.toDateString()) return t('timeline.today');
+ if (date.toDateString() === yesterday.toDateString()) return t('timeline.yesterday');
return date.toLocaleDateString('en-GB', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
@@ -676,11 +676,11 @@ function appendMessage(msg) {
if (msg.type === 'join' || msg.type === 'leave') {
el.classList.add('join-msg');
const color = getColor(msg.sender);
- el.innerHTML = `${escapeHtml(msg.sender)} ${msg.type === 'join' ? 'joined' : 'left'}`;
+ el.innerHTML = `${escapeHtml(msg.sender)} ${msg.type === 'join' ? t('timeline.joined') : t('timeline.left')}`;
} else if (msg.type === 'summary') {
el.classList.add('summary-msg');
const color = getColor(msg.sender);
- el.innerHTML = `Summary${escapeHtml(msg.sender)}${escapeHtml(msg.text)}
`;
+ el.innerHTML = `${t('timeline.summary')}${escapeHtml(msg.sender)}${escapeHtml(msg.text)}
`;
} else if (msg.type === 'job_proposal') {
el.classList.add('proposal-msg');
const meta = msg.metadata || {};
@@ -695,22 +695,22 @@ function appendMessage(msg) {
el.innerHTML = `
${title}
${body ? `
${body}
` : ''}
${isPending ? `
-
-
-
+
+
+
` : `
-
${status === 'accepted' ? 'Accepted' : 'Dismissed'}
+
${status === 'accepted' ? t('jobs.accepted') : t('jobs.dismissed')}
`}
- ${!isPending ? `` : ''}`;
+ ${!isPending ? `` : ''}`;
} else if (msg.type === 'rule_proposal') {
el.classList.add('proposal-msg');
const meta = msg.metadata || {};
@@ -721,21 +721,21 @@ function appendMessage(msg) {
el.innerHTML = `
${ruleText}
${isPending ? `
-
-
-
+
+
+
` : `
-
${status === 'activated' ? 'Activated' : status === 'drafted' ? 'Added to drafts' : 'Dismissed'}
+
${status === 'activated' ? t('rules.activated') : status === 'drafted' ? t('rules.addedDrafts') : t('jobs.dismissed')}
`}
- ${!isPending ? `` : ''}`;
+ ${!isPending ? `` : ''}`;
} else if (window._messageRenderers && window._messageRenderers[msg.type]) {
window._messageRenderers[msg.type](el, msg);
} else if (msg.type === 'system' || msg.sender === 'system') {
@@ -796,21 +796,21 @@ function appendMessage(msg) {
el.dataset.rawText = msg.text;
const senderRole = _agentRoles[msg.sender] || '';
const roleClass = senderRole ? 'bubble-role has-role' : 'bubble-role';
- const rolePillHtml = !isSelf ? `` : '';
+ const rolePillHtml = !isSelf ? `` : '';
// Inline decision choices (if present)
let choicesHtml = '';
const meta = msg.metadata || {};
const choicesList = meta.choices || [];
if (msg.type === 'decision' && choicesList.length > 0) {
if (meta.resolved) {
- choicesHtml = `You chose: ${escapeHtml(meta.chosen || '')}
`;
+ choicesHtml = `${escapeHtml(t('timeline.youChose', { choice: meta.chosen || '' }))}
`;
} else {
choicesHtml = '' + choicesList.map(c =>
``
).join('') + '
';
}
}
- el.innerHTML = `${isSelf ? '' : avatarHtml}${replyHtml}
${textHtml}
${choicesHtml}${attachmentsHtml}
`;
+ el.innerHTML = `${isSelf ? '' : avatarHtml}${replyHtml}
${textHtml}
${choicesHtml}${attachmentsHtml}
`;
if (todoStatus) el.classList.add('msg-todo', `msg-todo-${todoStatus}`);
if (msg.metadata?.session_output) el.classList.add('session-output');
@@ -1173,6 +1173,22 @@ const ROLE_PRESETS = [
{ label: 'Hype', emoji: '🎉' },
];
+function roleDisplayLabel(label) {
+ const keyByLabel = {
+ 'Planner': 'role.planner',
+ 'Designer': 'role.designer',
+ 'Architect': 'role.architect',
+ 'Builder': 'role.builder',
+ 'Reviewer': 'role.reviewer',
+ 'Researcher': 'role.researcher',
+ 'Red Team': 'role.redTeam',
+ 'Wry': 'role.wry',
+ 'Unhinged': 'role.unhinged',
+ 'Hype': 'role.hype',
+ };
+ return keyByLabel[label] ? t(keyByLabel[label]) : label;
+}
+
// --- Agent naming lightbox ---
const _pendingNameQueue = [];
@@ -1208,8 +1224,8 @@ function showAgentNameModal(opts) {
-
-
+
+
`;
// Close on backdrop click
@@ -1236,11 +1252,11 @@ function showAgentNameModal(opts) {
if (opts.mode === 'pending') {
const familyLabel = (baseColors[opts.base] || {}).label || opts.base || 'agent';
- titleEl.textContent = 'Name this agent';
- subtitleEl.textContent = `A new ${familyLabel} instance connected`;
+ titleEl.textContent = t('agent.nameThis');
+ subtitleEl.textContent = t('agent.newInstance', { family: familyLabel });
} else {
- titleEl.textContent = 'Rename agent';
- subtitleEl.textContent = `Current ID: @${opts.name}`;
+ titleEl.textContent = t('agent.rename');
+ subtitleEl.textContent = t('agent.currentId', { name: opts.name });
}
inputEl.value = opts.label;
@@ -1299,7 +1315,7 @@ function showPillPopover(pillEl, opts) {
const currentRole = (_agentRoles[opts.name] || '').toLowerCase();
const roleChipsHtml = ROLE_PRESETS.map(p =>
- ``
+ ``
).join('');
const customChipsHtml = (window.customRoles || [])
.filter(r => r && !ROLE_PRESETS.some(p => p.label.toLowerCase() === r.toLowerCase()))
@@ -1309,21 +1325,21 @@ function showPillPopover(pillEl, opts) {
popover.innerHTML = `
-
+
-
+
${(() => {
@@ -1339,15 +1355,15 @@ function showPillPopover(pillEl, opts) {
const matchesSwatch = swatches.some(c => current === c.toLowerCase());
const colorInputVal = (current && !current.startsWith('var(')) ? current : (opts.color || '#888888');
return ``;
})()}
@@ -1531,14 +1547,14 @@ function showBubbleRolePicker(btn, agentName) {
// None chip
const noneChip = document.createElement('button');
noneChip.className = 'role-preset-chip' + (!currentRole ? ' active' : '');
- noneChip.textContent = 'None';
+ noneChip.textContent = t('common.none');
noneChip.addEventListener('click', () => { _setRole(agentName, ''); closePicker(); });
picker.appendChild(noneChip);
for (const preset of ROLE_PRESETS) {
const chip = document.createElement('button');
chip.className = 'role-preset-chip' + (currentRole === preset.label.toLowerCase() ? ' active' : '');
- chip.textContent = `${preset.emoji} ${preset.label}`;
+ chip.textContent = `${preset.emoji} ${roleDisplayLabel(preset.label)}`;
chip.addEventListener('click', () => { _setRole(agentName, preset.label); closePicker(); });
picker.appendChild(chip);
}
@@ -1559,7 +1575,7 @@ function showBubbleRolePicker(btn, agentName) {
const customInput = document.createElement('input');
customInput.type = 'text';
customInput.className = 'bubble-role-input';
- customInput.placeholder = 'Custom...';
+ customInput.placeholder = t('agent.custom.placeholder');
customInput.maxLength = 20;
customInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
@@ -1793,7 +1809,7 @@ function _clearClearChatConfirm() {
const confirmEl = document.getElementById('clear-chat-confirm');
if (confirmEl) confirmEl.remove();
if (btn) {
- btn.textContent = 'Clear Chat';
+ btn.textContent = t('timeline.clearChat');
btn.classList.remove('confirming');
}
document.removeEventListener('click', _clearChatOutsideClick, true);
@@ -1823,17 +1839,17 @@ function clearChat() {
return;
}
- btn.textContent = 'Clear Chat?';
+ btn.textContent = t('timeline.clearChatConfirm');
btn.classList.add('confirming');
const confirmWrap = document.createElement('span');
confirmWrap.id = 'clear-chat-confirm';
confirmWrap.className = 'session-inline-confirm';
confirmWrap.innerHTML = `
-