Skip to content

Commit 82743ec

Browse files
committed
refactor(admin): switch AI generate prompt to imperative modal
Lift the generate-prompt UI out of the master-detail aside (where it occupied 70% of the viewport for a single language input) into the imperative modal layer via `presentGeneratePrompt`. The aside is now reserved for the edit flow, which needs the large surface for the Lexical editor.
1 parent a1267ea commit 82743ec

4 files changed

Lines changed: 118 additions & 128 deletions

File tree

apps/admin/src/features/ai/components/article-grouped/ArticleEditPanel.tsx

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,26 @@ import { X } from 'lucide-react'
22

33
import { useI18n } from '~/i18n'
44

5-
import { GeneratePromptBody } from './GeneratePromptBody'
65
import type { ArticleGroupedConfig } from './types'
76

87
interface ArticleEditPanelProps<TItem> {
98
config: ArticleGroupedConfig<TItem>
10-
mode: 'edit' | 'generate'
11-
editingItem: TItem | null
12-
selectedArticleId: string | null
13-
generateSubmitting: boolean
9+
editingItem: TItem
1410
updateSubmitting: boolean
1511
onClose: () => void
16-
onGenerate: (lang?: string) => Promise<unknown>
1712
onUpdate: (next: TItem) => Promise<void>
1813
}
1914

2015
export function ArticleEditPanel<TItem>(props: ArticleEditPanelProps<TItem>) {
2116
const { t } = useI18n()
22-
const { config, mode } = props
23-
const title =
24-
mode === 'generate' ? t(config.generate.labelKey) : t(config.editTitleKey)
17+
const { config } = props
2518

2619
return (
2720
<section className="flex h-full min-h-0 flex-col bg-surface-card">
2821
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-4">
29-
<h3 className="truncate text-sm font-medium text-fg">{title}</h3>
22+
<h3 className="truncate text-sm font-medium text-fg">
23+
{t(config.editTitleKey)}
24+
</h3>
3025
<button
3126
aria-label={t('ui.modal.closeAria')}
3227
className="inline-flex size-8 items-center justify-center rounded text-fg-muted transition-colors hover:bg-surface-inset hover:text-fg"
@@ -37,27 +32,13 @@ export function ArticleEditPanel<TItem>(props: ArticleEditPanelProps<TItem>) {
3732
</button>
3833
</header>
3934
<div className="min-h-0 flex-1">
40-
{mode === 'generate' && props.selectedArticleId ? (
41-
<GeneratePromptBody
42-
generateLabel={t(config.generate.labelKey)}
43-
inlineEmpty={t(config.inlineEmptyKey, { kind: t(config.kindKey) })}
44-
langLabel={t('ai.translation.langLabel')}
45-
onCancel={props.onClose}
46-
onSubmit={async ({ lang }) => {
47-
await props.onGenerate(lang)
48-
}}
49-
promptForLang={Boolean(config.generate.promptForLang)}
50-
submitting={props.generateSubmitting}
51-
/>
52-
) : mode === 'edit' && props.editingItem ? (
53-
<config.EditDrawerBody
54-
item={props.editingItem}
55-
key={config.getId(props.editingItem)}
56-
onCancel={props.onClose}
57-
onSubmit={props.onUpdate}
58-
submitting={props.updateSubmitting}
59-
/>
60-
) : null}
35+
<config.EditDrawerBody
36+
item={props.editingItem}
37+
key={config.getId(props.editingItem)}
38+
onCancel={props.onClose}
39+
onSubmit={props.onUpdate}
40+
submitting={props.updateSubmitting}
41+
/>
6142
</div>
6243
</section>
6344
)

apps/admin/src/features/ai/components/article-grouped/ArticleGroupedDetailRoute.tsx

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useArticleGroupedRouteContext } from './article-grouped-route-context'
1515
import { ArticleDetailEmptyState } from './ArticleDetailEmptyState'
1616
import { ArticleDetailPane } from './ArticleDetailPane'
1717
import { ArticleEditPanel } from './ArticleEditPanel'
18+
import { presentGeneratePrompt } from './GeneratePromptModal'
1819
import { useItemActions } from './useItemActions'
1920

2021
export function ArticleGroupedDetailRoute<TItem>() {
@@ -25,7 +26,6 @@ export function ArticleGroupedDetailRoute<TItem>() {
2526
const { config } = ctx
2627

2728
const [editingItemId, setEditingItemId] = useState<string | null>(null)
28-
const [generating, setGenerating] = useState(false)
2929

3030
const detailQuery = useQuery({
3131
enabled: Boolean(id),
@@ -100,7 +100,6 @@ export function ArticleGroupedDetailRoute<TItem>() {
100100
toast.success(
101101
result.created ? t('ai.toast.taskCreated') : t('ai.toast.taskExists'),
102102
)
103-
setGenerating(false)
104103
await ctx.invalidate()
105104
},
106105
})
@@ -126,6 +125,18 @@ export function ArticleGroupedDetailRoute<TItem>() {
126125
deleteMutation.mutate(config.getId(item))
127126
}
128127

128+
const handleGenerate = async () => {
129+
if (!id) return
130+
const result = await presentGeneratePrompt({
131+
inlineEmpty: t(config.inlineEmptyKey, { kind: t(config.kindKey) }),
132+
langLabel: t('ai.translation.langLabel'),
133+
promptForLang: Boolean(config.generate.promptForLang),
134+
title: t(config.generate.labelKey),
135+
})
136+
if (!result) return
137+
await generateMutation.mutateAsync({ refId: id, lang: result.lang })
138+
}
139+
129140
const { keyboardActions, buildMenu } = useItemActions<TItem>({
130141
config,
131142
onEdit: (item) => setEditingItemId(config.getId(item)),
@@ -140,10 +151,7 @@ export function ArticleGroupedDetailRoute<TItem>() {
140151
)
141152
}, [detailItems, editingItemId, config])
142153

143-
const closeEditPanel = () => {
144-
setEditingItemId(null)
145-
setGenerating(false)
146-
}
154+
const closeEditPanel = () => setEditingItemId(null)
147155

148156
if (!id || !detailArticle) {
149157
return (
@@ -159,13 +167,11 @@ export function ArticleGroupedDetailRoute<TItem>() {
159167
asideDefaultSize="70%"
160168
asideMaxSize="85%"
161169
asideMinSize="480px"
162-
asideMobileTitle={
163-
editingItem ? t(config.editTitleKey) : t(config.generate.labelKey)
164-
}
170+
asideMobileTitle={t(config.editTitleKey)}
165171
className="h-full"
166172
mainMinSize="280px"
167173
onCloseAside={closeEditPanel}
168-
open={Boolean(editingItem) || generating}
174+
open={Boolean(editingItem)}
169175
>
170176
<ArticleDetailPane<TItem>
171177
article={detailArticle}
@@ -177,36 +183,28 @@ export function ArticleGroupedDetailRoute<TItem>() {
177183
onBack={ctx.onBack}
178184
onDelete={(item) => void confirmAndDelete(item)}
179185
onEdit={(item) => setEditingItemId(config.getId(item))}
180-
onGenerate={() => setGenerating(true)}
186+
onGenerate={() => void handleGenerate()}
181187
onItemFocus={(item) => {
182188
if (editingItemId !== null) {
183189
setEditingItemId(config.getId(item))
184190
}
185191
}}
186192
/>
187-
<ContentLayoutSlot
188-
active={Boolean(editingItem) || generating}
189-
id="ai-article-edit"
190-
>
191-
<ArticleEditPanel<TItem>
192-
config={config}
193-
editingItem={editingItem}
194-
generateSubmitting={generateMutation.isPending}
195-
mode={editingItem ? 'edit' : 'generate'}
196-
onClose={closeEditPanel}
197-
onGenerate={async (lang) => {
198-
if (!id) return
199-
await generateMutation.mutateAsync({ refId: id, lang })
200-
}}
201-
onUpdate={async (next) => {
202-
await updateMutation.mutateAsync({
203-
id: config.getId(next),
204-
next,
205-
})
206-
}}
207-
selectedArticleId={id}
208-
updateSubmitting={updateMutation.isPending}
209-
/>
193+
<ContentLayoutSlot active={Boolean(editingItem)} id="ai-article-edit">
194+
{editingItem ? (
195+
<ArticleEditPanel<TItem>
196+
config={config}
197+
editingItem={editingItem}
198+
onClose={closeEditPanel}
199+
onUpdate={async (next) => {
200+
await updateMutation.mutateAsync({
201+
id: config.getId(next),
202+
next,
203+
})
204+
}}
205+
updateSubmitting={updateMutation.isPending}
206+
/>
207+
) : null}
210208
</ContentLayoutSlot>
211209
</ContentLayout>
212210
)

apps/admin/src/features/ai/components/article-grouped/GeneratePromptBody.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useState } from 'react'
2+
3+
import { useI18n } from '~/i18n'
4+
import { ModalFooter, ModalHeader } from '~/ui/feedback/modal'
5+
import { present, useModal } from '~/ui/feedback/modal-imperative'
6+
import { Button } from '~/ui/primitives/button'
7+
import { TextInput } from '~/ui/primitives/text-field'
8+
9+
export interface GeneratePromptModalProps {
10+
title: string
11+
promptForLang: boolean
12+
langLabel: string
13+
inlineEmpty?: string
14+
}
15+
16+
export interface GeneratePromptResult {
17+
lang?: string
18+
}
19+
20+
function GeneratePromptModal(props: GeneratePromptModalProps) {
21+
const { t } = useI18n()
22+
const modal = useModal<GeneratePromptResult>()
23+
const [lang, setLang] = useState('zh')
24+
25+
const handleSubmit = () => {
26+
if (props.promptForLang) {
27+
const trimmed = lang.trim().toLowerCase()
28+
if (!trimmed) return
29+
modal.close({ lang: trimmed })
30+
} else {
31+
modal.close({})
32+
}
33+
}
34+
35+
return (
36+
<div className="flex w-full flex-col">
37+
<ModalHeader title={props.title} />
38+
<div className="space-y-4 px-5 py-4">
39+
{props.promptForLang ? (
40+
<TextInput
41+
autoFocus
42+
label={props.langLabel}
43+
onChange={setLang}
44+
placeholder="zh"
45+
value={lang}
46+
/>
47+
) : (
48+
<p className="text-sm text-fg-muted">
49+
{props.inlineEmpty ?? props.title}
50+
</p>
51+
)}
52+
</div>
53+
<ModalFooter>
54+
<Button onClick={() => modal.dismiss()} type="button" variant="subtle">
55+
{t('common.cancel')}
56+
</Button>
57+
<Button onClick={handleSubmit} type="button" variant="primary">
58+
{props.title}
59+
</Button>
60+
</ModalFooter>
61+
</div>
62+
)
63+
}
64+
65+
export async function presentGeneratePrompt(
66+
props: GeneratePromptModalProps,
67+
): Promise<GeneratePromptResult | undefined> {
68+
const handle = present<GeneratePromptModalProps, GeneratePromptResult>(
69+
GeneratePromptModal,
70+
props,
71+
{ modalProps: { popupStyle: { width: 'min(92vw, 28rem)' } } },
72+
)
73+
return await handle
74+
}

0 commit comments

Comments
 (0)