feat: add note mode and workflow config

This commit is contained in:
shiyue
2026-06-12 16:39:44 +08:00
parent 5308a10b52
commit 8b2173be8f
93 changed files with 15292 additions and 50 deletions

View File

@@ -83,6 +83,7 @@
let reconnectTimer = null;
let pendingText = '';
let renderTimer = null;
let generatingSessionId = null;
let activeToolCalls = new Map();
let activeTodoCallTargets = new Map();
let toolDomSeq = 0;
@@ -109,6 +110,9 @@
let pendingNewSessionRequest = null;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false;
let noteMode = false;
let noteDraftSeq = 0;
const pendingNotesByTarget = new Map();
// --- DOM ---
const $ = (sel) => document.querySelector(sel);
@@ -142,10 +146,12 @@
const messagesDiv = $('#messages');
const msgInput = $('#msg-input');
const inputWrapper = msgInput.closest('.input-wrapper');
const noteModeBtn = $('#note-mode-btn');
const sendBtn = $('#send-btn');
const abortBtn = $('#abort-btn');
const cmdMenu = $('#cmd-menu');
const modeSelect = $('#mode-select');
const defaultMsgInputPlaceholder = msgInput.getAttribute('placeholder') || '输入消息… 输入 / 查看指令';
// --- Viewport height fix for mobile browsers ---
function setVH() {
@@ -164,6 +170,238 @@
return AGENT_LABELS[agent] ? agent : DEFAULT_AGENT;
}
function getDraftNoteKey(agent = currentAgent) {
return `draft:${normalizeAgent(agent)}`;
}
function getCurrentNoteKey(agent = currentAgent) {
return currentSessionId || getDraftNoteKey(agent);
}
function getNotesForKey(key, create = true) {
if (!key) return [];
if (!pendingNotesByTarget.has(key)) {
if (!create) return [];
pendingNotesByTarget.set(key, []);
}
return pendingNotesByTarget.get(key);
}
function getCurrentNotes(create = true) {
return getNotesForKey(getCurrentNoteKey(), create);
}
function cleanupNoteKey(key) {
const notes = pendingNotesByTarget.get(key);
if (!notes || notes.length === 0) pendingNotesByTarget.delete(key);
}
function migratePendingNotesToSession(sessionId, agent = currentAgent) {
if (!sessionId) return;
const draftKey = getDraftNoteKey(agent);
const draftNotes = pendingNotesByTarget.get(draftKey);
if (!draftNotes || draftNotes.length === 0) return;
const sessionNotes = getNotesForKey(sessionId, true);
sessionNotes.push(...draftNotes);
pendingNotesByTarget.delete(draftKey);
}
function updateNoteModeUI() {
const active = !!noteMode;
if (noteModeBtn) {
noteModeBtn.classList.toggle('active', active);
noteModeBtn.setAttribute('aria-pressed', active ? 'true' : 'false');
noteModeBtn.title = active ? '关闭笔记模式' : '笔记模式';
noteModeBtn.setAttribute('aria-label', active ? '关闭笔记模式' : '笔记模式');
}
if (inputWrapper) inputWrapper.classList.toggle('note-mode-active', active);
if (sendBtn) {
sendBtn.classList.toggle('note-send', active);
sendBtn.title = active ? '记录笔记' : '发送';
sendBtn.hidden = isGenerating ? !active : false;
}
if (msgInput) {
msgInput.placeholder = active ? '记录笔记,稍后从气泡发送…' : defaultMsgInputPlaceholder;
}
if (active) hideCmdMenu();
}
function createNoteActionButton(action, label, title = label) {
const button = document.createElement('button');
button.type = 'button';
button.className = `note-action ${action}`;
button.textContent = label;
button.title = title;
button.dataset.noteAction = action;
return button;
}
function createPendingNoteElement(note) {
const div = document.createElement('div');
div.className = 'msg note';
div.dataset.noteId = note.id;
const avatar = document.createElement('div');
avatar.className = 'msg-avatar note-avatar';
avatar.textContent = 'N';
const bubble = document.createElement('div');
bubble.className = 'msg-bubble note-bubble';
const meta = document.createElement('div');
meta.className = 'note-meta';
meta.textContent = '笔记 · 待发送';
const text = document.createElement('div');
text.className = 'note-text';
text.textContent = note.text;
const actions = document.createElement('div');
actions.className = 'note-actions';
const editBtn = createNoteActionButton('edit', '修改');
const deleteBtn = createNoteActionButton('delete', '删除');
const sendNoteBtn = createNoteActionButton('send', '发送', '发送这条笔记');
editBtn.addEventListener('click', () => beginEditPendingNote(note.id));
deleteBtn.addEventListener('click', () => removePendingNote(note.id));
sendNoteBtn.addEventListener('click', () => sendPendingNote(note.id));
actions.append(editBtn, deleteBtn, sendNoteBtn);
bubble.append(meta, text, actions);
div.append(avatar, bubble);
return div;
}
function renderPendingNotes(options = {}) {
messagesDiv.querySelectorAll('.msg.note').forEach((node) => node.remove());
const notes = getCurrentNotes(false);
if (!notes || notes.length === 0) {
updateScrollbar();
return;
}
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const frag = document.createDocumentFragment();
notes.forEach((note) => frag.appendChild(createPendingNoteElement(note)));
messagesDiv.appendChild(frag);
if (options.scroll !== false) {
scrollToBottom();
} else {
updateScrollbar();
}
}
function findPendingNote(noteId) {
const key = getCurrentNoteKey();
const notes = getNotesForKey(key, false);
const index = notes.findIndex((note) => note.id === noteId);
if (index === -1) return null;
return { key, notes, index, note: notes[index] };
}
function dropPendingNote(noteId) {
const found = findPendingNote(noteId);
if (!found) return null;
const [note] = found.notes.splice(found.index, 1);
cleanupNoteKey(found.key);
return note;
}
function addPendingNoteFromInput(text) {
const content = String(text || '').trim();
if (!content) return false;
const note = {
id: `note-${Date.now().toString(36)}-${++noteDraftSeq}`,
text: content,
createdAt: Date.now(),
};
getCurrentNotes(true).push(note);
renderPendingNotes();
return true;
}
function removePendingNote(noteId) {
if (!dropPendingNote(noteId)) return;
renderPendingNotes({ scroll: false });
}
function resizeNoteEditor(editor) {
editor.style.height = 'auto';
editor.style.height = Math.min(editor.scrollHeight, 180) + 'px';
}
function beginEditPendingNote(noteId) {
const found = findPendingNote(noteId);
if (!found) return;
const noteEl = messagesDiv.querySelector(`.msg.note[data-note-id="${noteId}"]`);
const bubble = noteEl?.querySelector('.note-bubble');
if (!bubble) return;
bubble.classList.add('editing');
bubble.innerHTML = '';
const meta = document.createElement('div');
meta.className = 'note-meta';
meta.textContent = '修改笔记';
const editor = document.createElement('textarea');
editor.className = 'note-edit-input';
editor.value = found.note.text;
editor.rows = 3;
const actions = document.createElement('div');
actions.className = 'note-actions';
const saveBtn = createNoteActionButton('save', '保存');
const cancelBtn = createNoteActionButton('cancel', '取消');
const save = () => {
const next = editor.value.trim();
if (!next) {
appendError('笔记内容不能为空。');
editor.focus();
return;
}
found.note.text = next;
renderPendingNotes();
};
saveBtn.addEventListener('click', save);
cancelBtn.addEventListener('click', () => renderPendingNotes({ scroll: false }));
editor.addEventListener('input', () => resizeNoteEditor(editor));
editor.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
save();
}
if (e.key === 'Escape') {
e.preventDefault();
renderPendingNotes({ scroll: false });
}
});
actions.append(saveBtn, cancelBtn);
bubble.append(meta, editor, actions);
requestAnimationFrame(() => {
resizeNoteEditor(editor);
editor.focus();
editor.select();
});
}
function sendPendingNote(noteId) {
if (isGenerating || isBlockingSessionLoad()) {
appendError('当前回复还在生成,稍后再发送笔记。');
return;
}
const note = dropPendingNote(noteId);
if (!note) return;
const text = String(note.text || '').trim();
renderPendingNotes({ scroll: false });
if (!text) return;
submitUserMessage(text);
}
function normalizeTheme(theme) {
return THEME_OPTIONS.some((item) => item.value === theme) ? theme : 'washi';
}
@@ -1182,12 +1420,40 @@
if (cached?.promise) return cached.promise;
const promise = (async () => {
await ensureAuthenticatedWs();
if (!authToken) {
const fetchAttachment = async () => {
await ensureAuthenticatedWs();
if (!authToken) {
throw new Error('登录状态已失效,请刷新页面后重新登录再预览图片。');
}
return fetch(`/api/attachments/${encodeURIComponent(id)}`, {
cache: 'no-store',
headers: {
Authorization: `Bearer ${authToken}`,
},
});
};
let response = await fetchAttachment();
if (response.status === 401 && localStorage.getItem('cc-web-pw')) {
authToken = null;
localStorage.removeItem('cc-web-token');
response = await fetchAttachment();
}
if (response.status === 401) {
throw new Error('登录状态已失效,请刷新页面后重新登录再预览图片。');
}
const url = `/api/attachments/${encodeURIComponent(id)}?token=${encodeURIComponent(authToken)}`;
attachmentPreviewCache.set(id, { url, objectUrl: false });
if (response.status === 404) {
throw new Error('图片不存在或已过期');
}
if (!response.ok) {
throw new Error('图片预览失败');
}
const blob = await response.blob();
if (!blob || blob.size === 0) {
throw new Error('图片预览失败');
}
const url = URL.createObjectURL(blob);
attachmentPreviewCache.set(id, { url, objectUrl: true });
return url;
})().catch((err) => {
attachmentPreviewCache.delete(id);
@@ -1296,18 +1562,20 @@
getAttachmentPreviewUrl(attachment)
.then((url) => {
if (!node.isConnected) return;
imgEl.src = url;
imgEl.onload = () => {
if (!node.isConnected) return;
imgEl.hidden = false;
placeholderEl.hidden = true;
node.classList.remove('is-error');
node.classList.add('is-loaded');
};
imgEl.onerror = () => {
if (!node.isConnected) return;
placeholderEl.textContent = '图片加载失败';
node.classList.remove('is-loaded');
node.classList.add('is-error');
};
imgEl.src = url;
})
.catch((err) => {
if (!node.isConnected) return;
@@ -1542,6 +1810,7 @@
if (getLastSessionForAgent(currentAgent) === session.id) {
localStorage.removeItem(getAgentSessionStorageKey(currentAgent));
}
pendingNotesByTarget.delete(session.id);
invalidateSessionCache(session.id);
send({ type: 'delete_session', sessionId: session.id });
if (session.id === currentSessionId) {
@@ -1641,7 +1910,9 @@
currentCwd = null;
currentModel = currentAgent === 'claude' ? 'opus' : '';
isGenerating = false;
generatingSessionId = null;
pendingText = '';
window.pendingContentBlocks = [];
pendingAttachments = [];
uploadingAttachments = [];
activeToolCalls.clear();
@@ -1653,20 +1924,24 @@
messagesDiv.innerHTML = buildWelcomeMarkup(currentAgent);
setStatsDisplay(null);
renderPendingAttachments();
renderPendingNotes({ scroll: false });
highlightActiveSession();
}
function applySessionSnapshot(snapshot, options = {}) {
if (!snapshot) return;
const snapshotAgent = normalizeAgent(snapshot.agent);
if (fileBrowserState && (fileBrowserState.sessionId !== snapshot.sessionId || (snapshot.cwd && fileBrowserState.rootPath !== snapshot.cwd))) {
closeFileBrowser();
}
const preserveStreaming = !!(options.preserveStreaming && isGenerating && snapshot.sessionId === currentSessionId && snapshot.isRunning);
if (isGenerating && !preserveStreaming) {
isGenerating = false;
generatingSessionId = null;
sendBtn.hidden = false;
abortBtn.hidden = true;
pendingText = '';
window.pendingContentBlocks = [];
activeToolCalls.clear();
activeTodoCallTargets.clear();
}
@@ -1674,7 +1949,8 @@
loadedHistorySessionId = snapshot.sessionId;
setLastSessionForAgent(snapshot.agent, currentSessionId);
chatTitle.textContent = snapshot.title || '新会话';
setCurrentAgent(snapshot.agent);
setCurrentAgent(snapshotAgent);
migratePendingNotesToSession(snapshot.sessionId, snapshotAgent);
setCurrentSessionRunningState(snapshot.isRunning);
setStatsDisplay(snapshot);
currentCwd = snapshot.cwd || null;
@@ -1687,6 +1963,8 @@
currentModel = snapshot.model || '';
if (!preserveStreaming) {
renderMessages(snapshot.messages || [], { immediate: !!options.immediate });
} else {
generatingSessionId = snapshot.sessionId;
}
highlightActiveSession();
renderSessionList();
@@ -1980,6 +2258,19 @@
}
// --- Server Message Handler ---
function isCurrentSessionEvent(msg) {
return !msg.sessionId || msg.sessionId === currentSessionId;
}
function ensureGeneratingForEvent(msg) {
if (!isCurrentSessionEvent(msg)) return false;
const targetSessionId = msg.sessionId || currentSessionId || null;
if (!isGenerating || generatingSessionId !== targetSessionId || !document.getElementById('streaming-msg')) {
return startGenerating(targetSessionId);
}
return true;
}
function handleServerMessage(msg) {
switch (msg.type) {
case 'auth_result':
@@ -2086,13 +2377,13 @@
break;
case 'text_delta':
if (!isGenerating) startGenerating();
if (!ensureGeneratingForEvent(msg)) break;
pendingText += msg.text;
scheduleRender();
break;
case 'content_blocks':
if (!isGenerating) startGenerating();
if (!ensureGeneratingForEvent(msg)) break;
if (Array.isArray(msg.blocks)) {
if (!window.pendingContentBlocks) window.pendingContentBlocks = [];
window.pendingContentBlocks.push(...msg.blocks);
@@ -2101,13 +2392,13 @@
break;
case 'tool_start':
if (!isGenerating) startGenerating();
if (!ensureGeneratingForEvent(msg)) break;
activeToolCalls.set(msg.toolUseId, { name: msg.name, input: msg.input, kind: msg.kind || null, meta: msg.meta || null, done: false });
appendToolCall(msg.toolUseId, msg.name, msg.input, false, msg.kind || null, msg.meta || null);
break;
case 'tool_update':
if (!isGenerating) startGenerating();
if (!ensureGeneratingForEvent(msg)) break;
if (!activeToolCalls.has(msg.toolUseId)) {
activeToolCalls.set(msg.toolUseId, {
name: msg.name,
@@ -2128,6 +2419,7 @@
break;
case 'tool_end':
if (!isCurrentSessionEvent(msg)) break;
if (activeToolCalls.has(msg.toolUseId)) {
activeToolCalls.get(msg.toolUseId).done = true;
if (msg.kind) activeToolCalls.get(msg.toolUseId).kind = msg.kind;
@@ -2138,6 +2430,7 @@
break;
case 'cost':
if (!isCurrentSessionEvent(msg)) break;
costDisplay.textContent = `$${msg.costUsd.toFixed(4)}`;
if (currentSessionId) {
updateCachedSession(currentSessionId, (snapshot) => { snapshot.totalCost = msg.costUsd; });
@@ -2145,6 +2438,7 @@
break;
case 'usage':
if (!isCurrentSessionEvent(msg)) break;
if (msg.totalUsage) {
const cacheText = msg.totalUsage.cachedInputTokens ? ` · cache ${msg.totalUsage.cachedInputTokens}` : '';
costDisplay.textContent = `in ${msg.totalUsage.inputTokens} · out ${msg.totalUsage.outputTokens}${cacheText}`;
@@ -2155,10 +2449,17 @@
break;
case 'done':
if (!isCurrentSessionEvent(msg)) {
if (msg.sessionId) {
updateCachedSession(msg.sessionId, (snapshot) => { snapshot.isRunning = false; });
}
break;
}
finishGenerating(msg.sessionId);
break;
case 'system_message':
if (!isCurrentSessionEvent(msg)) break;
appendSystemMessage(msg.message);
break;
@@ -2183,10 +2484,11 @@
break;
case 'resume_generating':
if (!isCurrentSessionEvent(msg)) break;
// Server has an active process for this session — resume streaming
setCurrentSessionRunningState(true);
if (!isGenerating || !document.getElementById('streaming-msg')) {
startGenerating();
startGenerating(msg.sessionId || currentSessionId);
} else {
sendBtn.hidden = true;
abortBtn.hidden = false;
@@ -2218,6 +2520,7 @@
break;
case 'error':
if (!isCurrentSessionEvent(msg)) break;
if (msg.code === 'new_session_cwd_missing' && pendingNewSessionRequest?.cwd) {
const request = pendingNewSessionRequest;
pendingNewSessionRequest = null;
@@ -2323,8 +2626,11 @@
}
// --- Generating State ---
function startGenerating() {
function startGenerating(sessionId = currentSessionId) {
const targetSessionId = sessionId || currentSessionId || null;
if (targetSessionId && currentSessionId && targetSessionId !== currentSessionId) return false;
isGenerating = true;
generatingSessionId = targetSessionId;
setCurrentSessionRunningState(true);
pendingText = '';
window.pendingContentBlocks = [];
@@ -2334,6 +2640,7 @@
hasGrouped = false;
sendBtn.hidden = true;
abortBtn.hidden = false;
updateNoteModeUI();
// 不禁用输入框,允许用户继续输入(但无法发送)
const welcome = messagesDiv.querySelector('.welcome-msg');
@@ -2341,6 +2648,7 @@
const msgEl = createMsgElement('assistant', '');
msgEl.id = 'streaming-msg';
if (targetSessionId) msgEl.dataset.sessionId = targetSessionId;
// 流式消息 bubble 拆为 .msg-text 和 .msg-tools 两个子容器
const bubble = msgEl.querySelector('.msg-bubble');
bubble.innerHTML = '';
@@ -2353,12 +2661,16 @@
bubble.appendChild(toolsDiv);
messagesDiv.appendChild(msgEl);
scrollToBottom();
return true;
}
function finishGenerating(sessionId) {
if (sessionId && currentSessionId && sessionId !== currentSessionId) return;
isGenerating = false;
generatingSessionId = null;
sendBtn.hidden = false;
abortBtn.hidden = true;
updateNoteModeUI();
setCurrentSessionRunningState(false);
msgInput.focus();
@@ -2400,12 +2712,15 @@
streamEl.removeAttribute('id');
}
if (sessionId) currentSessionId = sessionId;
if (sessionId) {
migratePendingNotesToSession(sessionId, currentAgent);
}
pendingText = '';
activeToolCalls.clear();
activeTodoCallTargets.clear();
toolGroupCount = 0;
hasGrouped = false;
renderPendingNotes();
}
// --- Rendering ---
@@ -2789,12 +3104,15 @@
messagesDiv.innerHTML = '';
if (messages.length === 0) {
messagesDiv.innerHTML = buildWelcomeMarkup(currentAgent);
renderPendingNotes({ scroll: false });
scrollToBottom();
return;
}
if (options.immediate) {
const frag = document.createDocumentFragment();
messages.forEach((message) => frag.appendChild(buildMsgElement(message)));
messagesDiv.appendChild(frag);
renderPendingNotes({ scroll: false });
scrollToBottom();
return;
}
@@ -2816,6 +3134,7 @@
const frag0 = document.createDocumentFragment();
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i]));
messagesDiv.appendChild(frag0);
renderPendingNotes({ scroll: false });
scrollToBottom();
// Render remaining batches asynchronously, prepending each
@@ -3819,8 +4138,33 @@
}
// --- Send Message ---
function submitUserMessage(text, attachments = []) {
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
messagesDiv.appendChild(createMsgElement('user', text, attachments));
scrollToBottom();
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
startGenerating();
}
function sendMessage() {
const text = msgInput.value.trim();
if (noteMode) {
if ((!text && pendingAttachments.length === 0) || isBlockingSessionLoad()) return;
hideCmdMenu();
hideOptionPicker();
if (pendingAttachments.length > 0) {
appendError('笔记模式暂不支持附带图片,请先移除图片。');
return;
}
if (addPendingNoteFromInput(text)) {
msgInput.value = '';
autoResize();
}
return;
}
if ((!text && pendingAttachments.length === 0) || isGenerating || isBlockingSessionLoad()) return;
hideCmdMenu();
hideOptionPicker();
@@ -3852,18 +4196,12 @@
}
// Regular message
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const attachments = pendingAttachments.map((attachment) => ({ ...attachment }));
messagesDiv.appendChild(createMsgElement('user', text, attachments));
scrollToBottom();
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
submitUserMessage(text, attachments);
msgInput.value = '';
pendingAttachments = [];
renderPendingAttachments();
autoResize();
startGenerating();
}
function autoResize() {
@@ -3947,6 +4285,13 @@
}
});
sendBtn.addEventListener('click', sendMessage);
if (noteModeBtn) {
noteModeBtn.addEventListener('click', () => {
noteMode = !noteMode;
updateNoteModeUI();
msgInput.focus();
});
}
abortBtn.addEventListener('click', () => send({ type: 'abort' }));
if (attachBtn && imageUploadInput) {
attachBtn.addEventListener('click', () => imageUploadInput.click());
@@ -3987,7 +4332,7 @@
autoResize();
const val = msgInput.value;
// Show slash command menu
if (val.startsWith('/') && !val.includes('\n')) {
if (!noteMode && val.startsWith('/') && !val.includes('\n')) {
showCmdMenu(val);
} else {
hideCmdMenu();
@@ -5583,6 +5928,7 @@
// --- Init ---
applyTheme(currentTheme);
setCurrentAgent(currentAgent);
updateNoteModeUI();
renderSessionList();
connect();
window.addEventListener('resize', updateCwdBadge);

View File

@@ -101,6 +101,13 @@
</svg>
</button>
<textarea id="msg-input" rows="1" placeholder="输入消息… 输入 / 查看指令" autocomplete="off"></textarea>
<button id="note-mode-btn" class="note-mode-btn" title="笔记模式" aria-label="笔记模式" aria-pressed="false" type="button">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
<path d="m15 5 3 3"></path>
</svg>
</button>
<button id="send-btn" class="send-btn" title="发送">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>

View File

@@ -21,6 +21,11 @@
--success: #5d8a54; /* 抹茶 matcha */
--danger: #c0553a;
--info: #5b7ea1; /* 縹色 blue-gray */
--note-accent: #5b7ea1;
--note-accent-hover: #486b8f;
--note-bg: rgba(91, 126, 161, 0.1);
--note-bg-strong: rgba(91, 126, 161, 0.16);
--note-border: rgba(91, 126, 161, 0.24);
--scrollbar-thumb: #c9baa9;
--scrollbar-track: transparent;
--sidebar-width: 280px;
@@ -62,6 +67,11 @@ html[data-theme='coolvibe'] {
--success: #2e8a61;
--danger: #d65567;
--info: #1976a4;
--note-accent: #2e8a61;
--note-accent-hover: #226547;
--note-bg: rgba(46, 138, 97, 0.1);
--note-bg-strong: rgba(46, 138, 97, 0.16);
--note-border: rgba(46, 138, 97, 0.24);
--scrollbar-thumb: #a7c4ce;
--font-ui: 'Chivo Mono', ui-monospace, monospace;
--page-background:
@@ -328,6 +338,11 @@ html[data-theme='editorial'] {
--success: #4d7b57;
--danger: #c05c42;
--info: #4f6f87;
--note-accent: #4f6f87;
--note-accent-hover: #3d596f;
--note-bg: rgba(79, 111, 135, 0.1);
--note-bg-strong: rgba(79, 111, 135, 0.16);
--note-border: rgba(79, 111, 135, 0.24);
--scrollbar-thumb: #bba995;
--font-ui: 'Avenir Next', 'Segoe UI', 'PingFang SC', sans-serif;
--page-background:
@@ -1259,6 +1274,98 @@ body.session-loading-active {
border-bottom-left-radius: 4px;
color: var(--text-primary);
}
.msg.note {
align-self: flex-end;
flex-direction: row-reverse;
max-width: min(680px, 85%);
}
.msg.note .note-avatar {
background: var(--note-accent);
color: #fff;
}
.msg.note .note-bubble {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.72), transparent),
var(--note-bg);
border: 1px solid var(--note-border);
border-bottom-right-radius: 4px;
color: var(--text-primary);
box-shadow: 0 10px 22px rgba(45, 31, 20, 0.05);
}
.note-meta {
margin-bottom: 6px;
color: var(--note-accent);
font-size: 11px;
font-weight: 800;
line-height: 1.2;
}
.note-text {
white-space: pre-wrap;
}
.note-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
margin-top: 10px;
}
.note-action {
appearance: none;
min-height: 30px;
padding: 0 10px;
border: 1px solid var(--note-border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.58);
color: var(--text-secondary);
cursor: pointer;
font: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1;
transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.1s;
}
.note-action:hover {
background: var(--note-bg-strong);
border-color: var(--note-accent);
color: var(--note-accent);
}
.note-action:active {
transform: scale(0.96);
}
.note-action.send,
.note-action.save {
background: var(--note-accent);
border-color: transparent;
color: #fff;
}
.note-action.send:hover,
.note-action.save:hover {
background: var(--note-accent-hover);
color: #fff;
}
.note-action.delete:hover {
background: rgba(192, 85, 58, 0.1);
border-color: rgba(192, 85, 58, 0.24);
color: var(--danger);
}
.note-edit-input {
width: min(460px, 100%);
min-width: 260px;
max-width: 100%;
border: 1px solid var(--note-border);
border-radius: 10px;
outline: none;
background: rgba(255, 255, 255, 0.76);
color: var(--text-primary);
font: inherit;
line-height: 1.5;
padding: 9px 10px;
resize: none;
}
.note-edit-input:focus {
border-color: var(--note-accent);
box-shadow: 0 0 0 3px var(--note-bg);
}
/* System messages */
.msg.system {
@@ -1844,6 +1951,10 @@ body.session-loading-active {
border-color: var(--info);
box-shadow: 0 0 0 3px rgba(91, 126, 161, 0.12);
}
.input-wrapper.note-mode-active {
border-color: var(--note-border);
box-shadow: 0 0 0 3px var(--note-bg);
}
.attach-btn {
appearance: none;
width: 40px;
@@ -1865,7 +1976,42 @@ body.session-loading-active {
background: rgba(192, 85, 58, 0.08);
border-color: rgba(192, 85, 58, 0.12);
}
.attach-btn:disabled {
.note-mode-btn {
appearance: none;
width: 36px;
height: 36px;
border: 1px solid transparent;
border-radius: 10px;
background: var(--note-accent);
color: #fff;
cursor: pointer;
padding: 0;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
}
.note-mode-btn:hover {
background: var(--note-accent-hover);
color: #fff;
border-color: transparent;
}
.note-mode-btn:active {
transform: scale(0.92);
}
.note-mode-btn.active {
background: var(--note-accent-hover);
border-color: transparent;
color: #fff;
box-shadow: 0 8px 18px var(--note-bg-strong);
}
.note-mode-btn.active:hover {
background: var(--note-accent-hover);
color: #fff;
}
.attach-btn:disabled,
.note-mode-btn:disabled {
opacity: 0.45;
cursor: default;
}
@@ -1904,6 +2050,13 @@ body.session-loading-active {
color: #fff;
}
.send-btn:hover { background: var(--accent-hover); }
.send-btn.note-send {
background: var(--note-accent);
color: #fff;
}
.send-btn.note-send:hover {
background: var(--note-accent-hover);
}
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.abort-btn {
background: var(--danger);
@@ -2024,8 +2177,13 @@ body.session-loading-active {
.msg.assistant .msg-bubble { border-bottom-left-radius: 4px; }
.code-block-wrapper pre code { font-size: 12px; }
.input-wrapper { padding: 6px 10px; border-radius: 12px; }
.attach-btn { width: 38px; height: 38px; border-radius: 11px; }
.attach-btn { width: 34px; height: 34px; border-radius: 10px; }
.note-mode-btn { width: 34px; height: 34px; border-radius: 10px; }
.send-btn, .abort-btn { width: 34px; height: 34px; }
.msg.note { max-width: 92%; }
.note-actions { gap: 5px; }
.note-action { min-height: 28px; padding: 0 8px; }
.note-edit-input { min-width: 0; }
.new-chat-btn,
.new-chat-arrow { min-height: 44px; }
.new-chat-arrow { width: 48px; }