diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b3c2bf51..768e4632 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -264,14 +264,18 @@ button:disabled { cursor: default; opacity: .45; } .new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 0 14px; border: 0; background: var(--color-surface-dark); color: var(--color-on-dark); cursor: pointer; font-size: var(--type-body-sm); transition: background-color 140ms ease-out, transform 120ms ease-out; margin: var(--space-4) 0 var(--space-6); border-radius: var(--radius-md); font-weight: 500; } .sidebar-label { display: block; padding: 0 var(--space-3) var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; } .session-list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-1); } -.session-row { position: relative; display: grid; gap: 4px; border-radius: var(--radius-md); color: var(--color-ink-secondary); } -.session-row > button { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: inherit; } +.session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 30px; align-items: center; border-radius: var(--radius-md); color: var(--color-ink-secondary); } +.session-row > button:first-child { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: inherit; } .session-row.is-active { color: var(--color-ink); background: var(--color-selected); } .session-row.is-active::before { position: absolute; border-radius: 3px; content: ""; top: var(--space-3); bottom: var(--space-3); left: var(--space-1); width: 2px; background: var(--color-action); } -.session-row > button > span { overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } -.session-row > button small { color: inherit; line-height: 1.3; opacity: .72; font-size: 12px; } -.session-actions { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 var(--space-2) var(--space-2) var(--space-4); } -.session-actions button { min-height: 26px; width: auto; padding: 0 7px; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; } +.session-row > button:first-child > span { overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } +.session-row > button:first-child small { color: inherit; line-height: 1.3; opacity: .72; font-size: 12px; } +.session-menu-trigger { width: 26px; height: 26px; display: grid; place-items: center; border: 0; border-radius: 999px; background: transparent; color: var(--color-ink-tertiary); cursor: pointer; opacity: 0; transition: opacity 120ms ease-out, background-color 120ms ease-out; } +.session-row:hover .session-menu-trigger, .session-row.is-active .session-menu-trigger, .session-menu-trigger[aria-expanded="true"] { opacity: 1; } +.session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: var(--color-canvas); color: var(--color-ink); } +.session-actions { position: absolute; z-index: 4; top: calc(100% - 4px); right: var(--space-1); min-width: 112px; display: grid; gap: 2px; padding: 6px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); box-shadow: var(--shadow-elevated); } +.session-actions button { min-height: 30px; width: 100%; padding: 0 8px; border: 0; border-radius: 8px; background: transparent; color: var(--color-ink-secondary); text-align: left; font-size: 12px; } +.session-actions button:hover { background: var(--color-canvas-muted); color: var(--color-ink); } .sidebar-footer { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--color-border); } .profile-trigger { width: 100%; display: grid; grid-template-columns: 34px minmax(0, 1fr) 18px; align-items: center; gap: 9px; padding: 5px 7px; border: 0; background: transparent; color: var(--color-ink); cursor: pointer; text-align: left; transition: background-color 120ms ease-out, transform 120ms ease-out; min-height: 56px; border-radius: var(--radius-md); } .profile-initial { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; font-size: 12px; text-transform: uppercase; border: 0; background: var(--color-action); color: var(--color-on-dark); font-weight: 500; } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 74ba4e51..65a85d49 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -669,6 +669,7 @@ export default function Home() { const [sessions, setSessions] = useState([]); const [pinnedSessionIds, setPinnedSessionIds] = useState([]); const [archivedSessionIds, setArchivedSessionIds] = useState([]); + const [sessionMenuId, setSessionMenuId] = useState(null); const [modelCatalog, setModelCatalog] = useState(null); const [activeSessionId, setActiveSessionId] = useState(""); const [draft, setDraft] = useState(""); @@ -1178,6 +1179,20 @@ export default function Home() { if (activeSessionId === sessionId) setActiveSessionId(visibleSessions.find((session) => session.id !== sessionId)?.id ?? ""); } + async function shareSession(session: ChatSession) { + const transcript = [ + `Jyotisha 对话:${session.title}`, + "", + ...session.messages.map((message) => `${message.role === "user" ? "我" : "Jyotisha"}:${message.text}`), + ].join("\n"); + try { + await navigator.clipboard.writeText(transcript); + setComposerNotice("已复制当前聊天,可粘贴转发。"); + } catch { + setComposerNotice("无法访问剪贴板,请手动复制聊天内容。"); + } + } + async function startNewChat() { if (!account || !modelCatalog || creatingSession) return; setMobileSidebarOpen(false); @@ -1992,13 +2007,21 @@ export default function Home() { 聊天记录
{visibleSessions.map((session) => ( -
+
{ + event.preventDefault(); + setSessionMenuId(sessionMenuId === session.id ? null : session.id); + }} + > -
- - - - -
+ + {sessionMenuId === session.id && ( +
+ + + + + +
+ )}
))}
diff --git a/tests/test_session_management_entrypoints.py b/tests/test_session_management_entrypoints.py index 580aa8ca..04f1460e 100644 --- a/tests/test_session_management_entrypoints.py +++ b/tests/test_session_management_entrypoints.py @@ -11,9 +11,14 @@ def test_chat_history_management_actions_are_exposed() -> None: "deleteSession", "togglePinnedSession", "toggleArchivedSession", + "shareSession", + "onContextMenu", + "session-menu-trigger", + 'role="menu"', "置顶", "重命名", "归档", "删除", + "转发", ): assert expected in source