Files
bilin/src/renderer/components/ai/AiSessionMenu.tsx
T
bing fe127ec3ed feat: ship v1.2.0 Wave 1 foundation with book settings, chapter management, and focus mode
Deliver schema v9, book/chapter IPC extensions, BookSettingsTab, chapter meta/tags,
AI session menu, focus mode and TTS, template context enrich, and Wave 1 E2E coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:22:58 +08:00

114 lines
3.7 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { AiSession } from '@shared/types'
import { ipcCall } from '@renderer/lib/ipc-client'
import { useAiStore } from '@renderer/stores/useAiStore'
interface AiSessionMenuProps {
bookId: string
session: AiSession | null
}
export function AiSessionMenu({ bookId, session }: AiSessionMenuProps): React.JSX.Element | null {
const { t } = useTranslation()
const loadSessions = useAiStore((s) => s.loadSessions)
const selectSession = useAiStore((s) => s.selectSession)
const [open, setOpen] = useState(false)
const [exportOpen, setExportOpen] = useState(false)
useEffect(() => {
if (!open) return
const close = (): void => {
setOpen(false)
setExportOpen(false)
}
window.addEventListener('click', close)
return () => window.removeEventListener('click', close)
}, [open])
if (!session) return null
const handleRename = async (): Promise<void> => {
setOpen(false)
const title = window.prompt(t('ai.session.renamePrompt'), session.title)
if (!title?.trim() || title.trim() === session.title) return
await ipcCall(() =>
window.electronAPI.ai.sessionUpdate(bookId, session.id, { title: title.trim() })
)
await loadSessions(bookId, true)
}
const handleArchiveToggle = async (): Promise<void> => {
setOpen(false)
await ipcCall(() =>
window.electronAPI.ai.sessionUpdate(bookId, session.id, { archived: !session.archived })
)
await loadSessions(bookId, true)
}
const handleExport = async (format: 'markdown' | 'txt'): Promise<void> => {
setOpen(false)
setExportOpen(false)
await ipcCall(() => window.electronAPI.ai.sessionExport(bookId, session.id, format))
}
const handleDelete = async (): Promise<void> => {
setOpen(false)
if (!window.confirm(t('ai.session.deleteConfirm', { title: session.title || t('ai.untitledSession') }))) {
return
}
await ipcCall(() => window.electronAPI.ai.sessionDelete(bookId, session.id))
await loadSessions(bookId, true)
const next = useAiStore.getState().sessions[0]
if (next) await selectSession(next.id)
}
return (
<div className="ai-session-menu-wrap">
<button
type="button"
className="btn ai-session-menu-btn"
data-testid="ai-session-menu-btn"
onClick={(e) => {
e.stopPropagation()
setOpen((v) => !v)
}}
>
</button>
{open && (
<div className="context-menu ai-session-menu" onClick={(e) => e.stopPropagation()}>
<button type="button" onClick={() => void handleRename()}>
{t('ai.session.rename')}
</button>
<button type="button" onClick={() => void handleArchiveToggle()}>
{session.archived ? t('ai.session.unarchive') : t('ai.session.archive')}
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setExportOpen((v) => !v)
}}
>
{t('ai.session.export')}
</button>
{exportOpen && (
<div className="ai-session-export-submenu">
<button type="button" onClick={() => void handleExport('markdown')}>
{t('ai.session.exportMd')}
</button>
<button type="button" onClick={() => void handleExport('txt')}>
{t('ai.session.exportTxt')}
</button>
</div>
)}
<button type="button" className="danger" onClick={() => void handleDelete()}>
{t('ai.session.delete')}
</button>
</div>
)}
</div>
)
}