返回 oh-my-ppt
AddPageDialog.tsx
根目录 / src / renderer / src / components / session-detail / modal / AddPageDialog.tsx
1 import { useEffect, useMemo, useState } from 'react'
2 import { useT } from '@renderer/i18n'
3 import { ipc } from '@renderer/lib/ipc'
4 import { useModelAction } from '@renderer/hooks/useModelAction'
5 import { useGenerateStore, useSessionDetailUiStore, useSessionStore } from '@renderer/store'
6 import { ModelSplitButton } from '../../model/ModelActionButton'
7 import { normalizePagesForSelection } from '../shared/pageUtils'
8
9 interface AddPageDialogProps {
10 sessionId: string
11 }
12
13 export function AddPageDialog({ sessionId }: AddPageDialogProps): React.JSX.Element | null {
14 const t = useT()
15 const modelAction = useModelAction()
16 const open = useSessionDetailUiStore((state) => state.addPageDialogOpen)
17 const selectedPageId = useSessionDetailUiStore((state) => state.selectedPageId)
18 const setOpen = useSessionDetailUiStore((state) => state.setAddPageDialogOpen)
19 const setIsAddingPage = useSessionDetailUiStore((state) => state.setIsAddingPage)
20 const setAddingPageId = useSessionDetailUiStore((state) => state.setAddingPageId)
21 const currentPages = useGenerateStore((state) => state.currentPages)
22 const loadSession = useSessionStore((state) => state.loadSession)
23 const [value, setValue] = useState('')
24
25 const normalizedPages = useMemo(() => normalizePagesForSelection(currentPages), [currentPages])
26 const selectedPage = useMemo(
27 () => normalizedPages.find((page) => page.id === selectedPageId) ?? normalizedPages[0] ?? null,
28 [normalizedPages, selectedPageId]
29 )
30
31 useEffect(() => {
32 if (open) setValue('')
33 }, [open])
34
35 const handleAddPage = async (selectedModelConfigId?: string): Promise<void> => {
36 if (!sessionId || !value.trim()) return
37 const description = value.trim()
38 setOpen(false)
39 setValue('')
40 const insertAfter = selectedPage?.pageNumber ?? normalizedPages.length
41 let targetSelection: string | null | undefined = undefined
42 let targetPageId: string | null = null
43 let started = false
44 let handedToJob = false
45
46 try {
47 const modelConfigId = await modelAction.ensureModelActive(selectedModelConfigId)
48 if (!modelConfigId) return
49 const sourcePageId = selectedPage?.id || normalizedPages.at(-1)?.id
50 if (!sourcePageId) return
51 const blankPage = await ipc.createBlankSessionPage({ sessionId, sourcePageId })
52 targetPageId = blankPage.selectedPageId
53 if (!targetPageId) throw new Error(t('sessionDetail.addPageFailed'))
54 targetSelection = targetPageId
55 setIsAddingPage(true)
56 setAddingPageId(targetPageId)
57 useGenerateStore.setState({ isGenerating: true, error: null, status: 'running' })
58 started = true
59 useGenerateStore
60 .getState()
61 .setPages(
62 blankPage.generatedPages.map((page) =>
63 page.id === targetPageId ? { ...page, status: 'generating' } : page
64 )
65 )
66 useSessionDetailUiStore.getState().setSelectedPageId(targetPageId)
67
68 const result = await ipc.addPage({
69 sessionId,
70 modelConfigId,
71 userMessage: description,
72 insertAfterPageNumber: insertAfter,
73 targetPageId
74 })
75 if (result.alreadyRunning) {
76 throw new Error(t('sessionDetail.addPageFailed'))
77 }
78 handedToJob = true
79 void ipc
80 .clearSpeechScript(sessionId)
81 .catch((err) => console.warn('[speech] clearSpeechScript failed', err))
82 } catch (err) {
83 const message = err instanceof Error ? err.message : t('sessionDetail.addPageFailed')
84 targetSelection = targetPageId
85 console.warn('[session-detail] add generated page failed', message)
86 } finally {
87 if (!handedToJob && started) {
88 if (targetPageId) {
89 try {
90 await loadSession(sessionId)
91 useGenerateStore.getState().setPages(useSessionStore.getState().currentGeneratedPages)
92 } catch (error) {
93 console.warn('[session-detail] reload blank page failed', error)
94 }
95 }
96 useSessionDetailUiStore.getState().finishAddPage(targetSelection)
97 useGenerateStore.getState().finishGeneration()
98 }
99 }
100 }
101
102 if (!open) return null
103
104 return (
105 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
106 <div className="w-[520px] rounded-2xl bg-white p-6 shadow-2xl">
107 <h3 className="mb-3 text-base font-semibold text-[#2f3a2a]">
108 {t('sessionDetail.addPage')}
109 </h3>
110 <p className="mb-3 text-xs text-[#8a9a7b]">{t('sessionDetail.addPageHint')}</p>
111 <textarea
112 value={value}
113 onChange={(event) => setValue(event.target.value)}
114 placeholder={t('sessionDetail.addPageDescription')}
115 className="mb-4 h-40 w-full resize-none rounded-xl border border-[#d4e4c1]/60 bg-[#f8f6f0] px-4 py-3 text-sm leading-relaxed text-[#2f3a2a] placeholder:text-[#8a9a7b] focus:border-[#5d6b4d] focus:outline-none"
116 autoFocus
117 onKeyDown={(event) => {
118 if (
119 event.key === 'Enter' &&
120 !event.shiftKey &&
121 !event.nativeEvent.isComposing &&
122 value.trim()
123 ) {
124 event.preventDefault()
125 void handleAddPage()
126 }
127 if (event.key === 'Escape') {
128 setOpen(false)
129 }
130 }}
131 />
132 <div className="flex items-center justify-end gap-2">
133 <button
134 type="button"
135 onClick={() => setOpen(false)}
136 className="cursor-pointer rounded-xl px-4 py-2 text-sm font-medium text-[#5d6b4d] transition-colors hover:bg-[#f0ece3]"
137 >
138 {t('sessionDetail.addPageCancel')}
139 </button>
140 <ModelSplitButton
141 modelAction={modelAction}
142 label={t('sessionDetail.addPageGenerate')}
143 disabled={!value.trim()}
144 tone="primary"
145 size="sm"
146 className="rounded-xl"
147 mainClassName="min-w-[104px] justify-center text-sm"
148 triggerClassName="h-9"
149 onRun={(modelConfigId) => void handleAddPage(modelConfigId)}
150 />
151 </div>
152 </div>
153 </div>
154 )
155 }
156
156 lines Plain Text