Skip to content
Closed
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
8 changes: 8 additions & 0 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ vision = false
return { ok: true };
});

ipcMain.handle("emrg:renameSession", async (_e, { sessionId, title }) => {
if (!validateSessionId(sessionId)) throw new Error("invalid session_id");
const clean = String(title || "").trim().slice(0, 80); // 截断超长标题
if (!clean) throw new Error("empty title");
const frame = await client.sendCommandAndWait("rename_session", { session_id: sessionId, cwd: projectDir, title: clean }, 5000);
return { ok: true, title: frame.title || clean };
});

ipcMain.handle("emrg:newSession", async () => {
// G14/G81:本地生成 session_id(无 new_session 消息)
const sid = generateSessionId();
Expand Down
1 change: 1 addition & 0 deletions emrg/gui/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const api = {
switchSession: (payload) => ipcRenderer.invoke("emrg:switchSession", payload),
newSession: () => ipcRenderer.invoke("emrg:newSession"),
deleteSession: (payload) => ipcRenderer.invoke("emrg:deleteSession", payload),
renameSession: (payload) => ipcRenderer.invoke("emrg:renameSession", payload),
setModel: (payload) => ipcRenderer.invoke("emrg:setModel", payload),
listModels: () => ipcRenderer.invoke("emrg:listModels"),
saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload),
Expand Down
91 changes: 61 additions & 30 deletions emrg/gui/renderer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ async function newSession() {
// 本地切订阅(resume 不存在的新会话会被 daemon 自动创建)
state.sessionId = sid;
clearChat();
addSystemMessage(`新会话 ${sid}`);
showWelcomeScreen(); // P3:空状态欢迎屏(设计 §3.5)
await refreshSessions();
highlightActiveSession(sid);
} catch (e) {
Expand All @@ -189,21 +189,8 @@ async function deleteSession(sid) {
addSystemMessage("当前有进行中的响应,请等待完成或停止后再删除会话。");
return;
}
if (!confirm(`确定删除会话 ${sid}?`)) return; // G76
try {
await window.emrg.deleteSession({ sessionId: sid });
if (state.sessionId === sid) {
const remaining = state.sessions.filter((s) => s.session_id !== sid);
if (remaining.length > 0) {
await switchSession(remaining[0].session_id, { silent: true });
} else {
await newSession();
}
}
await refreshSessions();
} catch (e) {
addSystemMessage(`删除失败: ${e.message}`);
}
// P3:删除走友好确认对话框(sidebar.js),不再用原生 confirm()
await requestDeleteSession(sid);
}

async function refreshSessions() {
Expand Down Expand Up @@ -341,6 +328,7 @@ function addSystemMessage(text) {
}

function appendMsg(node) {
hideWelcomeScreen(); // P3:首条真实消息到达即移除欢迎屏
$("chat-view").appendChild(node);
scrollToBottom();
}
Expand All @@ -351,6 +339,45 @@ function clearChat() {
state.toolCards.clear();
}

// ── 空状态欢迎屏(P3,设计 §3.5)──────────────────────

const WELCOME_SUGGESTIONS = [
{ icon: "📝", text: "帮我写一份周报" },
{ icon: "🗂", text: "整理这个文件夹" },
{ icon: "✈️", text: "规划一次旅行" },
];

function showWelcomeScreen() {
const cv = $("chat-view");
cv.innerHTML = "";
const wrap = document.createElement("div");
wrap.className = "welcome";
const cards = WELCOME_SUGGESTIONS.map(
(s) => `<button class="welcome-card" data-text="${escapeHtml(s.text)}"><span class="welcome-icon">${s.icon}</span><span>${escapeHtml(s.text)}</span></button>`
).join("");
wrap.innerHTML = `
<div class="welcome-mark">✦</div>
<div class="welcome-title">你好,我是 EMRG</div>
<div class="welcome-sub">我可以帮你写作、整理、查资料、处理文件…</div>
<div class="welcome-cards">${cards}</div>
`;
for (const card of wrap.querySelectorAll(".welcome-card")) {
card.addEventListener("click", () => {
const input = $("input");
input.value = card.dataset.text;
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 150) + "px";
input.focus();
});
}
cv.appendChild(wrap);
}

function hideWelcomeScreen() {
const w = $("chat-view").querySelector(".welcome");
if (w) w.remove();
}

function scrollToBottom() {
if (state.autoScroll) {
const cv = $("chat-view");
Expand All @@ -366,25 +393,29 @@ function renderSessions(sessions) {
list.innerHTML = "<div class='session-item placeholder'>暂无会话</div>";
return;
}
for (const s of state.sessions) {
const item = document.createElement("div");
item.className = "session-item";
const title = s.title || s.session_id; // G27:title 优先 session_id 兜底
const count = s.message_count || 0;
item.innerHTML = `<span class="sess-title">${escapeHtml(title)}</span><span class="sess-count">${count} msgs</span>`;
item.addEventListener("click", () => switchSession(s.session_id));
item.addEventListener("contextmenu", (e) => {
e.preventDefault();
deleteSession(s.session_id);
});
list.appendChild(item);
// P3:按时间分组(今天/昨天/更早),只显示标题(设计 §3.2——不显示 session ID/消息数)
for (const [label, group] of groupSessionsByTime(state.sessions)) {
if (!group.length) continue;
const gl = document.createElement("div");
gl.className = "session-group-label";
gl.textContent = label;
list.appendChild(gl);
for (const s of group) {
const item = document.createElement("div");
item.className = "session-item";
item.dataset.sid = s.session_id; // P3:右键菜单定位
const title = s.title || s.session_id; // G27:title 优先 session_id 兜底
item.innerHTML = `<span class="sess-title">${escapeHtml(title)}</span>`;
item.addEventListener("click", () => switchSession(s.session_id));
list.appendChild(item);
}
}
highlightActiveSession(state.sessionId);
}

function highlightActiveSession(sid) {
for (const item of $("session-list").children) {
item.classList.toggle("active", item.textContent.includes(sid) && item.classList.contains("session-item"));
for (const item of $("session-list").querySelectorAll(".session-item")) {
item.classList.toggle("active", item.dataset.sid === sid);
}
}

Expand Down
79 changes: 77 additions & 2 deletions emrg/gui/renderer/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.sess-count { color: var(--text-3); font-size: 11px; flex-shrink: 0; }
.session-item.active .sess-count { color: var(--accent); }

/* ── 消息 ──────────────────────────── */
.msg {
Expand Down Expand Up @@ -197,6 +195,83 @@ dialog .desc { font-size: var(--text-aux); color: var(--text-2); margin-bottom:
.dir-row input { flex: 1; }
.dir-row button { flex-shrink: 0; }

/* ── 空状态欢迎屏(P3,设计 §3.5)───────────────── */
.welcome {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-3);
text-align: center;
padding: var(--space-5);
animation: msg-in var(--dur-med) var(--ease);
}
.welcome-mark {
font-size: 44px;
color: var(--accent);
line-height: 1;
margin-bottom: var(--space-2);
}
.welcome-title { font-size: 20px; font-weight: 600; color: var(--text-1); }
.welcome-sub { font-size: var(--text-secondary); color: var(--text-2); }
.welcome-cards {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-top: var(--space-4);
width: 100%;
max-width: 340px;
}
.welcome-card {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-card);
color: var(--text-1);
font-size: var(--text-secondary);
text-align: left;
box-shadow: var(--shadow-sm);
transition: border-color var(--dur-fast), box-shadow var(--dur-fast), transform var(--dur-fast);
}
.welcome-card:hover {
border-color: var(--accent);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.welcome-icon { font-size: 18px; }

/* ── 右键菜单(P3,设计 §3.2)────────────────── */
.ctx-menu {
position: fixed;
z-index: 1000;
min-width: 150px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-card);
box-shadow: var(--shadow-lg);
padding: 4px;
animation: msg-in var(--dur-fast) var(--ease);
}
.ctx-item {
padding: 9px 12px;
border-radius: 8px;
font-size: var(--text-secondary);
color: var(--text-1);
cursor: pointer;
transition: background var(--dur-fast);
}
.ctx-item:hover { background: var(--bg-soft); }
.ctx-item.danger { color: var(--red); }
.ctx-item.danger:hover { background: color-mix(in srgb, var(--red) 10%, transparent); }

/* ── 对话框危险按钮 ─────────────────────────── */
.dialog-actions button.danger { background: var(--red); border-color: var(--red); color: #fff; font-weight: 600; }
.dialog-actions button.danger:hover { filter: brightness(1.08); }

/* 消息入场动效:轻微上浮 + 淡入(克制) */
@keyframes msg-in {
from { opacity: 0; transform: translateY(8px); }
Expand Down
31 changes: 31 additions & 0 deletions emrg/gui/renderer/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@
.brand-mark { color: var(--accent); font-size: 20px; line-height: 1; }
.brand-name { font-size: 17px; font-weight: 600; color: var(--text-1); letter-spacing: 0.2px; }

/* 折叠按钮(⌘B):hover 出现,轻柔 */
.btn-sidebar-toggle {
margin-left: auto;
width: 26px;
height: 26px;
border-radius: 8px;
border: none;
background: none;
color: var(--text-3);
font-size: 12px;
line-height: 1;
transition: background var(--dur-fast), color var(--dur-fast);
}
.btn-sidebar-toggle:hover { background: var(--bg-soft); color: var(--text-1); }

/* 折叠态:只留品牌区(窄条),其余隐藏 */
#sidebar.collapsed {
width: 52px;
min-width: 52px;
}
#sidebar.collapsed .brand-name,
#sidebar.collapsed .btn-new,
#sidebar.collapsed #session-list,
#sidebar.collapsed .sidebar-footer .btn-settings {
display: none;
}
#sidebar.collapsed .brand { justify-content: center; padding: var(--space-3) 0; }
#sidebar.collapsed .btn-sidebar-toggle { margin-left: 0; }
#sidebar.collapsed .sidebar-footer { justify-content: center; padding: var(--space-3) 0; }
#sidebar.collapsed .conn-dot { margin: 0 auto; }

/* 新对话按钮 */
.btn-new {
margin: 0 var(--space-4) var(--space-3);
Expand Down
29 changes: 29 additions & 0 deletions emrg/gui/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<div class="brand">
<span class="brand-mark">✦</span>
<span class="brand-name">EMRG</span>
<button id="sidebar-toggle" class="btn-sidebar-toggle" title="折叠侧边栏 (⌘B)">◂</button>
</div>
<button id="new-session-btn" class="btn-new" title="新对话">+ 新对话</button>
<div id="session-list">
Expand Down Expand Up @@ -111,11 +112,39 @@ <h2>欢迎使用 EMRG</h2>
</form>
</dialog>

<!-- 右键菜单(P3 sidebar:重命名 / 删除) -->
<div id="ctx-menu" class="ctx-menu" hidden></div>

<!-- 确认对话框(友好文案,替代 confirm()) -->
<dialog id="confirm-dialog">
<h2 id="confirm-title">确认操作</h2>
<p id="confirm-desc" class="desc"></p>
<div class="dialog-actions">
<button type="button" id="confirm-cancel">取消</button>
<button type="button" id="confirm-ok" class="danger">确认</button>
</div>
</dialog>

<!-- 重命名对话框 -->
<dialog id="rename-dialog">
<h2>重命名对话</h2>
<div class="form-row">
<label>对话标题
<input type="text" id="rename-input" maxlength="80" placeholder="给这段对话起个名字" />
</label>
</div>
<div class="dialog-actions">
<button type="button" id="rename-cancel">取消</button>
<button type="button" id="rename-ok" class="primary">保存</button>
</div>
</dialog>

<script src="../vendor/marked.min.js"></script>
<script src="../vendor/dompurify.min.js"></script>
<script src="../vendor/highlight.custom.js"></script>
<script src="markdown.js"></script>
<script src="js/copywriting.js"></script>
<script src="js/sidebar.js"></script>
<script src="app.js"></script>
</body>
</html>
Loading
Loading