Skip to content

Commit d8343e5

Browse files
authored
feat(ai): multi-language + force regeneration in generate modal, and per-article AI overview board (#2804)
* docs(spec): 单文章 AI 汇总看板设计 * feat(ai): add force regeneration semantics to summary/insights/translation tasks Thread force through the summary/insights/translation task payloads and generation pipelines, mirroring the existing TTS force behavior: dedup keys split force vs incremental so a forced task cannot be swallowed by an in-flight regular one, and AiInFlightService.runWithStream gains bypassResultCache to skip (and evict) a cached result on force. Translation additionally drops block-level incremental reuse on force while still reading the existing row for its other bookkeeping uses. * fix(ai): propagate force into the PartialFailed translation retry payload buildTranslationRetryTask rebuilt retryPayload without the original task's force flag, so a force task that partially failed would silently retry as incremental and hit the in-flight result cache instead of regenerating. * feat(admin): support multi-language input and force regen in AI generate modal parseLangInput normalizes comma-separated (incl. full-width comma) lang input into a deduped, ordered list. GeneratePromptModal now renders live lang chips with a count, caps input at 8 languages, and adds a "force regenerate" checkbox shown across all four generate flows. Call sites are updated in a follow-up task. * fix(admin): only gate too-many-langs on the langs prompt branch tooMany was computed unconditionally from the parsed lang input, but langs are only actually submitted when promptForLang is true. A caller passing defaultLangs with >8 entries alongside promptForLang: false silently disabled the submit button with no visible error. * chore: revert accidental page.tsx rename picked up by shared-worktree commit An unrelated in-progress rename (apps/admin/src/views/(intelligence)/ai/page.tsx -> ai/overview/page.tsx) from a concurrent session was staged in the shared index and got swept into the prior commit. This restores the tracked path to its pre-existing location without touching any working-tree file contents, so that session's uncommitted work is unaffected. * feat(admin): wire multi-lang + force into AI generate call sites Connects GeneratePromptModal's {langs, force} result to all four AI generate flows (summary, translation, tts, insights) plus the quick actions menu and the article detail generate button. Adds useAiDefaultLangs to prefill the modal from the AI config's summary/translation target-language settings. createSummaryTask now takes targetLanguages (array) instead of a single lang, matching the backend DTO; createInsightsTask and createTranslationTask gain force. Inline retranslate/regenerate row actions always pass force: true. * fix(admin): add missing useAiGenerateTask hook referenced by c7b038c * feat(admin): per-article AI overview board Adds a reverse view of the AI surfaces: given one article, which assets exist, what is missing, and what the generation has cost. Server: GET /ai/overview/grouped lists every article (posts, notes, pages) newest first with a compact per-capability language projection; GET /ai/overview/article/:id returns the assets, a per-resource-type cost roll-up summed across every generation ever recorded, and the AI tasks currently in flight for that article. Admin: /ai/overview holds a coverage matrix of capability x language. An empty cell dispatches the generation task, a filled one scrolls to its asset row, and a queued one shows a spinner so a second click cannot duplicate the work. Languages beyond the configured targets can be added as ad-hoc columns. * fix(ai): close inflight bypass race window and cap targetLanguages at 8 runWithStream's bypassResultCache path deleted resultKey before attempting the lock, opening a window where an unrelated concurrent follower for the same key could observe resultKey, errorKey, and lockKey all empty and throw. Defer the delete until this instance actually holds lockKey, and skip reading the cached result entirely on bypass instead of racing a delete against the read. Also give summary/translation task DTOs the same targetLanguages cap TTS already enforces at runtime, sourced from one shared MAX_LANGS_PER_TASK constant instead of a second literal. * refactor(admin): rename runWithLangPrompt to runWithGeneratePrompt The insights quick-action passes promptForLang: false yet calls the same runWithLangPrompt helper, discarding its first arg as _langs. Rename only — the helper drives every generate-prompt flow, not just the language-prompting ones. * fix(ai): fold force into the summary/insights/translation in-flight key buildSummaryKey/buildInsightsKey/buildTranslationKey hashed only articleId/lang/content, so a force request racing an in-flight plain request for the same key lost the lock, became a follower, and silently replayed the plain leader's incremental output — force appeared to succeed but never regenerated anything. Fold force into each hash so force and plain requests never share a lock/stream/result key; repeated force requests still converge on the same key. * docs: add admin AI generate modal multilang design spec Records the design for the multi-language generate prompt modal and core force-regeneration semantics, updated with the actual in-flight key behavior (force and plain requests now use independent Redis lock/stream/result keys instead of the originally-accepted "force follows plain" degradation). * feat(admin): active task list, language add control and overview sections Extends the per-article AI overview board with an active-task list, an add-language control and a grouped overview section; core side gains active-task derivation for the overview payload. * fix(admin): keep the AI overview polling until a generation actually settles Clicking a coverage cell left a spinner that never resolved. Two causes, both found by driving the board in a browser against a dev server whose AI feature was off, so every task failed within a millisecond: - refetchInterval is suspended while the window is unfocused, so the one fetch that caught the task mid-queue was also the last one. The board then displayed that frozen snapshot forever. refetchIntervalInBackground keeps a progress view honest when the user switches away. - An optimistic pending entry bridged the gap before the queue registers a task, retired by conditions that did not always hold. It could outlive every one of them and become a permanent phantom. The server now reports live tasks and recent failures alike, so the client no longer invents state; a dispatch grace window keeps polling alive until the queue has something to say. Polling now runs while anything is live or within 20s of a dispatch, and stops once the queue is quiet. * fix(ai): restore single in-flight lock and normalize target language codes Folding `force` into the summary/insights/translation in-flight key hash (8433347) fixed force silently joining a plain leader, but split the mutex too: a force and a plain request for the same content now each ran the model and upserted the same row, with whichever finished last clobbering the other and doubling the bill. Revert the key change and arbitrate on lock ownership instead. The lock value now encodes the holder's mode (`force:`/`plain:`). A force request that loses the race to a plain leader polls until the lock frees up (or lockTtlSec elapses, degrading to a follower) instead of racing it with a second writer; a lock already held by another force is joined immediately, since two force runs converging on one leader is the desired outcome. Plain-request behavior on a lost race is unchanged. Also fold resolved target languages through parseLanguageCode before dedup in the summary and translation task handlers, matching what TTS already does — zh-CN and zh no longer produce two separate generations for the same input. * fix(admin): fold region-suffixed language codes in AI generate modal input parseLangInput treated zh-CN and zh (or en_US and EN) as distinct languages, so the multilang chips could lie about how many generations would actually run. Normalize underscores to hyphens and drop a 2-letter primary tag's region suffix before dedup — matching the backend's authoritative parseLanguageCode without duplicating its alias table here. * fix(ai): re-check lock holder each poll while a force request waits waitForForceLock only inspected the lock holder once, when the initial NX attempt failed. If a plain leader released the lock mid-wait and a different force request won it via the ordinary leader path, this instance kept polling blind until lockTtlSec instead of noticing the lock was now force-held and joining immediately. Re-check the holder after each failed retry so a mid-wait handoff to another force is picked up right away, saving a redundant model call and up to lockTtlSec of unnecessary waiting. Behavior when the holder stays plain (or the lock is released outright) is unchanged. * fix(ai): clear streamKey/errorKey when a force leader acquires the lock Acquiring the lock only cleared resultKey before starting a fresh run. streamKey (done frames live 600s) and errorKey (30s) from a previous run on the same key survived, so a follower or converging force joining this leader reads the stream from '0-0' and can hit the old `done`/`error` entry first — resolving to the previous run's result or throwing a stale error instead of waiting for the new one. Delete all three together, still gated behind the lock so a concurrent plain follower never observes them all empty at once. * fix(ai): normalize target langs without clobbering unrecognized tokens Two related language-normalization bugs, fixed together since they touch the same call sites. parseLanguageCode's fallback for anything it doesn't recognize is DEFAULT_SUMMARY_LANG ('zh'). Folding summary/translation target languages through it meant a free-typed token like "english" (the admin generate modal takes arbitrary input) collapsed onto 'zh' and silently overwrote an actual zh row instead of just being its own odd entry. Add normalizeTargetLang (ai-language.util.ts): known codes/aliases still fold via normalizeLanguageCode, but anything unrecognized passes through as trim+lowercase instead of defaulting. Use it in both the summary task handler and executeTranslationTask. Since executeTranslationTask now generates against normalized language codes, buildTranslationRetryTask's PartialFailed diff broke: it compared raw payload.targetLanguages (e.g. 'zh-CN') against already-normalized result.translations[].lang ('zh'), so a successful zh-CN run never matched and got retried (with force inherited, at extra cost). Normalize payload.targetLanguages the same way before diffing. * fix(ai): reject blank target-language tokens instead of coining '' normalizeTargetLang trimmed a blank/whitespace-only token and then, since normalizeLanguageCode returns undefined for it, fell back to the trimmed (still empty) string — so an empty target language silently became a language named ''. The public task DTOs don't reject it either (CreateSummaryTaskSchema / CreateTranslationTaskSchema only checked z.string(), no non-empty constraint), so any direct API caller (not just the admin modal, which already filters client-side) could push targetLanguages: [''] through and generate against it. Two-sided fix: normalizeTargetLang now returns undefined for a blank token instead of '', and both call sites (summary handler, executeTranslationTask) filter it out — dropping it rather than defaulting it to DEFAULT_SUMMARY_LANG, which would reintroduce the same silent-overwrite problem the unrecognized-token fix just closed. The DTOs add `.trim().min(1)` per element so a blank entry 400s at the boundary instead of reaching the task handler at all. * fix(ai): normalize target langs before hashing the summary/translation dedup key computeAITaskDedupKey canonicalized languages for Tts (parseLanguageCode) but not for Summary/Translation, which just sorted+joined the raw targetLanguages. Now that generation itself normalizes (zh-CN and zh produce the same result), two requests differing only in region suffix enqueue as two distinct tasks at the queue layer — the loser runs for nothing. Add canonicalTargetLangs, using the same normalizeTargetLang the handlers generate against (not parseLanguageCode — that would be a third normalization scheme for the same data), and use it for the Summary and Translation branches. Tts keeps its existing parseLanguageCode-based canonicalization unchanged. * fix(ai): give each in-flight leader run its own stream key A forced regeneration used to delete the shared stream the moment it took the lock, so a follower still draining the finished run lost its tail and spliced the new run's tokens into the same response. Streams are now keyed by run (`:stream:<runId>`, resolved from the lock value), so a completed generation stays readable until its TTL retires it and force only clears the result/error cache. Run-scoped keys also retire the stale-frame hazard the delete existed for. Also settles the follower result promise a lock-race test left dangling: its idle timeout rejected ~1s later and failed the whole shard. * fix(ai): canonicalize languages and carry force/targets through the overview board - coverage compares canonicalized configured targets, so a `zh-CN` setting no longer reports a gap the stored `zh` row can never close - the article's active tasks include batch children and are looked up by refId, so a busy queue can no longer push a live task off the first page - insights translation accepts force end to end (payload, DTO, dedup key, in-flight bypass) - an insights task carries the language requested from a cell with no base row and chains its translation once the base exists - retry re-dispatches every language the task ran on, and keeps the "use configured targets" case as an empty list instead of guessing one - the language alias table moves to @mx-space/ai so admin folds exactly as the server does * feat(ai): unify summary and insights behind a shared multilang base-then-translate pipeline - ai_summaries gains is_translation/source_summary_id/source_lang, lang backfill, dedup and UNIQUE(ref_id, lang) via migration 0032 - new ai-multilang MultilangAdapter + MultilangGenerationService: reuse or generate the source-language base, invalidate stale translations, then translate remaining targets concurrently - insights drops chained queue subtasks for inline concurrent translation and resolves its source lang from meta.lang instead of the nonexistent article.lang - summary drops the per-language loop, adds SummaryTranslation task type, POST /ai/summaries/task/translate and SUMMARY_GENERATED - insights task DTO gains the 8-language cap and blank-token rejection; overview and admin dispatch follow the base/translation split * refactor(admin): dispatch insights like summary — base first, translation only for a single-language retry
1 parent c84d0bb commit d8343e5

104 files changed

Lines changed: 15798 additions & 1004 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/admin/src/api/ai-overview.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { ArticleInfo, GenerationMetrics, PaginationInfo } from './ai'
2+
import { getJson } from './http'
3+
4+
export const AI_OVERVIEW_CAPABILITIES = [
5+
'summary',
6+
'insights',
7+
'translation',
8+
'tts',
9+
] as const
10+
11+
export type AiOverviewCapability = (typeof AI_OVERVIEW_CAPABILITIES)[number]
12+
13+
export interface CapabilityCoverage {
14+
applicable: boolean
15+
expected: string[]
16+
langs: string[]
17+
}
18+
19+
export type ArticleCoverage = Record<
20+
AiOverviewCapability,
21+
CapabilityCoverage
22+
> & {
23+
sourceLang: string | null
24+
}
25+
26+
export interface AiOverviewListRow {
27+
article: ArticleInfo
28+
coverage: ArticleCoverage
29+
gapCount: number
30+
}
31+
32+
export interface AiOverviewListResponse {
33+
data: AiOverviewListRow[]
34+
pagination: PaginationInfo
35+
}
36+
37+
interface AssetBase {
38+
createdAt: string
39+
generationMetrics?: GenerationMetrics | null
40+
id: string
41+
lang: string
42+
}
43+
44+
export interface SummaryAsset extends AssetBase {
45+
isTranslation: boolean
46+
sourceLang: string | null
47+
summary: string
48+
}
49+
50+
export interface InsightsAsset extends AssetBase {
51+
content: string
52+
isTranslation: boolean
53+
sourceLang: string | null
54+
}
55+
56+
export interface TranslationAsset extends AssetBase {
57+
aiModel: string | null
58+
sourceLang: string
59+
updatedAt: string | null
60+
}
61+
62+
export interface TtsAsset extends AssetBase {
63+
charCount: number
64+
durationMs: number | null
65+
isTranslation: boolean
66+
updatedAt: string | null
67+
}
68+
69+
export interface CostBucket {
70+
cacheReadTokens: number
71+
cacheWriteTokens: number
72+
costTotalUsd: number
73+
generationCount: number
74+
inputTokens: number
75+
outputTokens: number
76+
totalTokens: number
77+
}
78+
79+
export const ACTIVE_TASK_STATUSES = ['pending', 'running'] as const
80+
81+
export interface ActiveGeneration {
82+
capability: AiOverviewCapability
83+
completedItems: number | null
84+
error: string | null
85+
langs: string[]
86+
progress: number | null
87+
progressMessage: string | null
88+
startedAt: number | null
89+
status: string
90+
taskId: string
91+
totalItems: number | null
92+
}
93+
94+
export interface AiOverviewDetail {
95+
activeTasks: ActiveGeneration[]
96+
article: ArticleInfo
97+
assets: {
98+
insights: InsightsAsset[]
99+
summary: SummaryAsset[]
100+
translation: TranslationAsset[]
101+
tts: TtsAsset[]
102+
}
103+
cost: {
104+
byResourceType: Record<AiOverviewCapability, CostBucket>
105+
models: string[]
106+
total: CostBucket
107+
}
108+
coverage: ArticleCoverage
109+
}
110+
111+
export const OVERVIEW_ARTICLE_TYPES = ['post', 'note', 'page'] as const
112+
113+
export type OverviewArticleType = (typeof OVERVIEW_ARTICLE_TYPES)[number]
114+
115+
export function getOverviewGrouped(params?: {
116+
page?: number
117+
search?: string
118+
size?: number
119+
type?: OverviewArticleType
120+
}) {
121+
return getJson<AiOverviewListResponse>('/ai/overview/grouped', params)
122+
}
123+
124+
export function getArticleOverview(refId: string) {
125+
return getJson<AiOverviewDetail>(`/ai/overview/article/${refId}`)
126+
}

apps/admin/src/api/ai.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,24 @@ export function updateSummary(id: string, data: { summary: string }) {
280280
return patchJson<AISummary, { summary: string }>(`/ai/summaries/${id}`, data)
281281
}
282282

283-
export function createSummaryTask(data: { lang?: string; refId: string }) {
284-
return postJson<CreateTaskResponse, { lang?: string; refId: string }>(
285-
'/ai/summaries/task',
283+
export function createSummaryTask(data: {
284+
force?: boolean
285+
refId: string
286+
targetLanguages?: string[]
287+
}) {
288+
return postJson<
289+
CreateTaskResponse,
290+
{ force?: boolean; refId: string; targetLanguages?: string[] }
291+
>('/ai/summaries/task', data)
292+
}
293+
294+
export function createSummaryTranslationTask(data: {
295+
force?: boolean
296+
refId: string
297+
targetLang: string
298+
}) {
299+
return postJson<CreateTaskResponse, typeof data>(
300+
'/ai/summaries/task/translate',
286301
data,
287302
)
288303
}
@@ -307,18 +322,20 @@ export function updateInsights(id: string, data: { content: string }) {
307322
return patchJson<AIInsights, { content: string }>(`/ai/insights/${id}`, data)
308323
}
309324

310-
export function createInsightsTask(data: { refId: string }) {
311-
return postJson<CreateTaskResponse, { refId: string }>(
312-
'/ai/insights/task',
313-
data,
314-
)
325+
export function createInsightsTask(data: {
326+
force?: boolean
327+
refId: string
328+
targetLanguages?: string[]
329+
}) {
330+
return postJson<CreateTaskResponse, typeof data>('/ai/insights/task', data)
315331
}
316332

317333
export function createInsightsTranslationTask(data: {
334+
force?: boolean
318335
refId: string
319336
targetLang: string
320337
}) {
321-
return postJson<CreateTaskResponse, { refId: string; targetLang: string }>(
338+
return postJson<CreateTaskResponse, typeof data>(
322339
'/ai/insights/task/translate',
323340
data,
324341
)
@@ -378,12 +395,13 @@ export function updateTranslation(
378395
}
379396

380397
export function createTranslationTask(data: {
398+
force?: boolean
381399
refId: string
382400
targetLanguages?: string[]
383401
}) {
384402
return postJson<
385403
CreateTaskResponse,
386-
{ refId: string; targetLanguages?: string[] }
404+
{ force?: boolean; refId: string; targetLanguages?: string[] }
387405
>('/ai/translations/task', data)
388406
}
389407

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { adminQueryKeys } from '~/query/keys'
1010
import { confirmDialog } from '~/ui/feedback/confirm'
1111
import { ContentLayout, ContentLayoutSlot } from '~/ui/layout/content-layout'
1212

13+
import { useAiDefaultLangs } from '../../hooks/use-ai-default-langs'
1314
import { getErrorMessage } from '../../utils/ai'
1415
import { useArticleGroupedRouteContext } from './article-grouped-route-context'
1516
import { ArticleDetailEmptyState } from './ArticleDetailEmptyState'
@@ -26,6 +27,7 @@ export function ArticleGroupedDetailRoute<TItem>() {
2627
const queryClient = useQueryClient()
2728
const ctx = useArticleGroupedRouteContext<TItem>()
2829
const { config } = ctx
30+
const defaultLangs = useAiDefaultLangs(config.generate.defaultLangsOptionKey)
2931

3032
const [editingItemId, setEditingItemId] = useState<string | null>(null)
3133

@@ -94,7 +96,7 @@ export function ArticleGroupedDetailRoute<TItem>() {
9496
})
9597

9698
const generateMutation = useMutation({
97-
mutationFn: (input: { refId: string; lang?: string }) =>
99+
mutationFn: (input: { refId: string; langs?: string[]; force?: boolean }) =>
98100
config.generate.runTask(input),
99101
onError: (error: unknown) =>
100102
toast.error(getErrorMessage(error, t('ai.toast.taskCreateFailed'))),
@@ -130,13 +132,18 @@ export function ArticleGroupedDetailRoute<TItem>() {
130132
const handleGenerate = async () => {
131133
if (!id) return
132134
const result = await presentGeneratePrompt({
135+
defaultLangs,
133136
inlineEmpty: t(config.inlineEmptyKey, { kind: t(config.kindKey) }),
134-
langLabel: t('ai.translation.langLabel'),
137+
langLabel: t('ai.generate.langsLabel'),
135138
promptForLang: Boolean(config.generate.promptForLang),
136139
title: t(config.generate.labelKey),
137140
})
138141
if (!result) return
139-
await generateMutation.mutateAsync({ refId: id, lang: result.lang })
142+
await generateMutation.mutateAsync({
143+
force: result.force,
144+
langs: result.langs,
145+
refId: id,
146+
})
140147
}
141148

142149
const { keyboardActions, buildMenu } = useItemActions<TItem>({

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

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,102 @@
11
import type { FormEvent } from 'react'
22
import { useState } from 'react'
33

4+
import { SmallBadge } from '~/features/tasks/components/TaskPrimitives'
45
import { useI18n } from '~/i18n'
56
import { ModalFooter, ModalHeader } from '~/ui/feedback/modal'
67
import { present, useModal } from '~/ui/feedback/modal-imperative'
78
import { Button } from '~/ui/primitives/button'
9+
import { Checkbox } from '~/ui/primitives/checkbox'
810
import { TextInput } from '~/ui/primitives/text-field'
911

12+
import { parseLangInput } from '../../utils/ai'
13+
14+
const MAX_LANGS = 8
15+
1016
export interface GeneratePromptModalProps {
1117
title: string
1218
promptForLang: boolean
1319
langLabel: string
1420
inlineEmpty?: string
21+
defaultLangs?: string[]
1522
}
1623

1724
export interface GeneratePromptResult {
18-
lang?: string
25+
langs: string[]
26+
force: boolean
1927
}
2028

2129
function GeneratePromptModal(props: GeneratePromptModalProps) {
2230
const { t } = useI18n()
2331
const modal = useModal<GeneratePromptResult>()
24-
const [lang, setLang] = useState('zh')
32+
const [langInput, setLangInput] = useState(
33+
props.defaultLangs?.join(', ') ?? '',
34+
)
35+
const [force, setForce] = useState(false)
36+
37+
const langs = parseLangInput(langInput)
38+
const tooMany = props.promptForLang && langs.length > MAX_LANGS
2539

2640
const handleSubmit = (event?: FormEvent) => {
2741
event?.preventDefault()
28-
if (props.promptForLang) {
29-
const trimmed = lang.trim().toLowerCase()
30-
if (!trimmed) return
31-
modal.close({ lang: trimmed })
32-
} else {
33-
modal.close({})
34-
}
42+
if (tooMany) return
43+
modal.close({
44+
force,
45+
langs: props.promptForLang ? langs : [],
46+
})
3547
}
3648

3749
return (
3850
<form className="flex w-full flex-col" onSubmit={handleSubmit}>
3951
<ModalHeader title={props.title} />
4052
<div className="space-y-4 px-5 py-4">
4153
{props.promptForLang ? (
42-
<TextInput
43-
autoFocus
44-
label={props.langLabel}
45-
onChange={setLang}
46-
placeholder="zh"
47-
value={lang}
48-
/>
54+
<div className="grid gap-1.5 text-sm">
55+
<TextInput
56+
autoFocus
57+
label={props.langLabel}
58+
onChange={setLangInput}
59+
placeholder="zh, en, ja"
60+
value={langInput}
61+
/>
62+
<p className="text-xs text-fg-muted">
63+
{t('ai.generate.langsHint')}
64+
</p>
65+
{langs.length > 0 ? (
66+
<div className="flex flex-wrap items-center gap-1.5">
67+
{langs.map((lang) => (
68+
<SmallBadge key={lang}>{lang}</SmallBadge>
69+
))}
70+
<span className="text-xs text-fg-muted">
71+
{t('ai.generate.langsCount', { count: langs.length })}
72+
</span>
73+
</div>
74+
) : null}
75+
{tooMany ? (
76+
<span className="text-xs text-red-500">
77+
{t('ai.generate.langsTooMany', { max: MAX_LANGS })}
78+
</span>
79+
) : null}
80+
</div>
4981
) : (
5082
<p className="text-sm text-fg-muted">
5183
{props.inlineEmpty ?? props.title}
5284
</p>
5385
)}
86+
<div className="grid gap-1">
87+
<Checkbox
88+
checked={force}
89+
label={t('ai.generate.forceLabel')}
90+
onCheckedChange={setForce}
91+
/>
92+
<p className="text-xs text-fg-muted">{t('ai.generate.forceHint')}</p>
93+
</div>
5494
</div>
5595
<ModalFooter>
5696
<Button onClick={() => modal.dismiss()} type="button" variant="subtle">
5797
{t('common.cancel')}
5898
</Button>
59-
<Button type="submit" variant="primary">
99+
<Button disabled={tooMany} type="submit" variant="primary">
60100
{props.title}
61101
</Button>
62102
</ModalFooter>

apps/admin/src/features/ai/components/article-grouped/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,12 @@ export interface ArticleGroupedConfig<TItem> {
6868
labelKey: TranslationKey
6969
icon: LucideIcon
7070
promptForLang?: boolean
71+
defaultLangsOptionKey?:
72+
'summaryTargetLanguages' | 'translationTargetLanguages'
7173
runTask: (input: {
7274
refId: string
73-
lang?: string
75+
langs?: string[]
76+
force?: boolean
7477
}) => Promise<{ created: boolean; taskId: string }>
7578
taskTypeForQueue: 'Insights' | 'Summary' | 'Translation' | 'Tts'
7679
}

0 commit comments

Comments
 (0)