feat(article): support regenerating failed articles
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
ReloadOutlined,
|
||||
SendOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -22,6 +23,11 @@ withDefaults(
|
||||
showCopy?: boolean
|
||||
canCopy?: boolean
|
||||
copyDisabledTitle?: string
|
||||
showRegenerate?: boolean
|
||||
canRegenerate?: boolean
|
||||
regenerateDisabledTitle?: string
|
||||
regenerateConfirmTitle?: string
|
||||
regenerateLoading?: boolean
|
||||
showDelete?: boolean
|
||||
canDelete?: boolean
|
||||
deleteDisabledTitle?: string
|
||||
@@ -41,6 +47,11 @@ withDefaults(
|
||||
showCopy: false,
|
||||
canCopy: true,
|
||||
copyDisabledTitle: '',
|
||||
showRegenerate: false,
|
||||
canRegenerate: true,
|
||||
regenerateDisabledTitle: '',
|
||||
regenerateConfirmTitle: '',
|
||||
regenerateLoading: false,
|
||||
showDelete: true,
|
||||
canDelete: true,
|
||||
deleteDisabledTitle: '',
|
||||
@@ -54,6 +65,7 @@ const emit = defineEmits<{
|
||||
edit: []
|
||||
preview: []
|
||||
copy: []
|
||||
regenerate: []
|
||||
delete: []
|
||||
}>()
|
||||
|
||||
@@ -135,6 +147,43 @@ function resolveTitle(enabled: boolean, enabledTitle: string, disabledTitle?: st
|
||||
</span>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip
|
||||
v-if="showRegenerate && !regenerateConfirmTitle"
|
||||
:title="resolveTitle(canRegenerate, t('common.regenerate'), regenerateDisabledTitle)"
|
||||
>
|
||||
<span>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-regenerate"
|
||||
:disabled="!canRegenerate"
|
||||
:loading="regenerateLoading"
|
||||
@click="emit('regenerate')"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</a-button>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
|
||||
<a-popconfirm
|
||||
v-else-if="showRegenerate"
|
||||
:title="regenerateConfirmTitle"
|
||||
:disabled="!canRegenerate"
|
||||
@confirm="emit('regenerate')"
|
||||
>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-regenerate"
|
||||
:disabled="!canRegenerate"
|
||||
:loading="regenerateLoading"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
<a-tooltip
|
||||
v-if="showDelete && !deleteConfirmTitle"
|
||||
:title="resolveTitle(canDelete, t('common.delete'), deleteDisabledTitle)"
|
||||
@@ -195,6 +244,11 @@ function resolveTitle(enabled: boolean, enabledTitle: string, disabledTitle?: st
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
.action-regenerate.ant-btn:hover:not(:disabled) {
|
||||
color: #fa8c16;
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.action-delete.ant-btn:hover:not(:disabled) {
|
||||
color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
|
||||
@@ -15,6 +15,7 @@ import ArticleSourceMeta from '@/components/ArticleSourceMeta.vue'
|
||||
import PublishArticleModal from '@/components/PublishArticleModal.vue'
|
||||
import { articlesApi, promptRulesApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { getGenerateStatusMeta, getPublishStatusMeta } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { formatPublishPlatformList } from '@/lib/publish-platforms'
|
||||
@@ -110,6 +111,8 @@ const deleteMutation = useMutation({
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
})
|
||||
|
||||
const { regeneratingArticleId, handleRegenerate } = useArticleRegenerateAction()
|
||||
|
||||
const columns = computed<TableColumnsType<ArticleListItem>>(() => [
|
||||
{ title: t('custom.article.titleColumn'), dataIndex: 'title', key: 'title', width: 340 },
|
||||
{
|
||||
@@ -353,13 +356,16 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<ArticleActionGroup
|
||||
v-bind="resolveArticleActionState(record.generate_status)"
|
||||
v-bind="resolveArticleActionState(record)"
|
||||
:delete-confirm-title="t('templates.list.deleteConfirm')"
|
||||
:delete-loading="deleteMutation.isPending.value"
|
||||
:regenerate-confirm-title="t('common.regenerateConfirm')"
|
||||
:regenerate-loading="regeneratingArticleId === record.id"
|
||||
@publish="openPublish(record)"
|
||||
@edit="openEditor(record)"
|
||||
@preview="openDetail(record)"
|
||||
@copy="copyArticle(record.id)"
|
||||
@regenerate="handleRegenerate(record)"
|
||||
@delete="deleteMutation.mutate(record.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -51,6 +51,10 @@ const enUS = {
|
||||
upgrade: 'Upgrade',
|
||||
copy: 'Copy',
|
||||
copySuccess: 'Content copied',
|
||||
regenerate: 'Regenerate',
|
||||
regenerateConfirm: 'Regenerate this article?',
|
||||
regenerateSuccess: 'Regeneration queued',
|
||||
regenerateError: 'Regeneration failed, please try again',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
imageUploadProgress: {
|
||||
|
||||
@@ -51,6 +51,10 @@ const zhCN = {
|
||||
upgrade: "去升级",
|
||||
copy: "复制",
|
||||
copySuccess: "内容已复制",
|
||||
regenerate: "重新生成",
|
||||
regenerateConfirm: "确认重新生成这篇文章吗?",
|
||||
regenerateSuccess: "重新生成已提交",
|
||||
regenerateError: "重新生成失败,请稍后重试",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
imageUploadProgress: {
|
||||
|
||||
@@ -13,16 +13,16 @@ import type {
|
||||
BrandRequest,
|
||||
Competitor,
|
||||
CompetitorRequest,
|
||||
CreateArticleRequest,
|
||||
CreateKolPackageRequest,
|
||||
CreateKolPromptRequest,
|
||||
CreateAckRequest,
|
||||
ComplianceAckRecord,
|
||||
ComplianceCheckArticleRequest,
|
||||
ComplianceCheckPlainRequest,
|
||||
ComplianceCheckResult,
|
||||
ComplianceManualReview,
|
||||
ComplianceRuntimeStatus,
|
||||
CreateAckRequest,
|
||||
CreateArticleRequest,
|
||||
CreateKolPackageRequest,
|
||||
CreateKolPromptRequest,
|
||||
CreatePluginSessionRequest,
|
||||
CreatePluginSessionResponse,
|
||||
CreatePublishBatchRequest,
|
||||
@@ -92,6 +92,7 @@ import type {
|
||||
QuotaSummary,
|
||||
RecentArticle,
|
||||
RefreshResponse,
|
||||
RegenerateArticleResponse,
|
||||
RegisterPluginInstallationRequest,
|
||||
RegisterPluginInstallationResponse,
|
||||
ScheduleTask,
|
||||
@@ -626,6 +627,12 @@ export const articlesApi = {
|
||||
payload,
|
||||
)
|
||||
},
|
||||
regenerate(id: number) {
|
||||
return apiClient.post<RegenerateArticleResponse, null>(
|
||||
`/api/tenant/articles/${id}/regenerate`,
|
||||
null,
|
||||
)
|
||||
},
|
||||
detail(id: number) {
|
||||
return apiClient.get<ArticleDetail>(`/api/tenant/articles/${id}`)
|
||||
},
|
||||
@@ -1115,7 +1122,9 @@ export const complianceApi = {
|
||||
)
|
||||
},
|
||||
latestArticleCheck(articleId: number) {
|
||||
return apiClient.get<ComplianceCheckResult>(`/api/tenant/articles/${articleId}/compliance/latest`)
|
||||
return apiClient.get<ComplianceCheckResult>(
|
||||
`/api/tenant/articles/${articleId}/compliance/latest`,
|
||||
)
|
||||
},
|
||||
createAck(recordId: number, payload: CreateAckRequest) {
|
||||
return apiClient.post<ComplianceAckRecord, CreateAckRequest>(
|
||||
|
||||
@@ -6,9 +6,26 @@ export interface ArticleActionState {
|
||||
showPreview: boolean
|
||||
showCopy: boolean
|
||||
showDelete: boolean
|
||||
showRegenerate: boolean
|
||||
}
|
||||
|
||||
export function resolveArticleActionState(generateStatus?: string | null): ArticleActionState {
|
||||
export interface ArticleActionSource {
|
||||
generate_status?: string | null
|
||||
source_type?: string | null
|
||||
}
|
||||
|
||||
const regeneratableSourceTypes = new Set(['template', 'custom_generation', 'imitation', 'kol'])
|
||||
|
||||
export function resolveArticleActionState(
|
||||
articleOrGenerateStatus?: ArticleActionSource | string | null,
|
||||
): ArticleActionState {
|
||||
const generateStatus =
|
||||
typeof articleOrGenerateStatus === 'string'
|
||||
? articleOrGenerateStatus
|
||||
: articleOrGenerateStatus?.generate_status
|
||||
const sourceType =
|
||||
typeof articleOrGenerateStatus === 'string' ? undefined : articleOrGenerateStatus?.source_type
|
||||
|
||||
if (generateStatus === 'completed') {
|
||||
return {
|
||||
showPublish: true,
|
||||
@@ -16,6 +33,7 @@ export function resolveArticleActionState(generateStatus?: string | null): Artic
|
||||
showPreview: true,
|
||||
showCopy: true,
|
||||
showDelete: true,
|
||||
showRegenerate: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +44,7 @@ export function resolveArticleActionState(generateStatus?: string | null): Artic
|
||||
showPreview: false,
|
||||
showCopy: false,
|
||||
showDelete: true,
|
||||
showRegenerate: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +55,7 @@ export function resolveArticleActionState(generateStatus?: string | null): Artic
|
||||
showPreview: false,
|
||||
showCopy: false,
|
||||
showDelete: true,
|
||||
showRegenerate: sourceType ? regeneratableSourceTypes.has(sourceType) : false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +65,7 @@ export function resolveArticleActionState(generateStatus?: string | null): Artic
|
||||
showPreview: true,
|
||||
showCopy: false,
|
||||
showDelete: true,
|
||||
showRegenerate: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ArticleListItem, RecentArticle } from '@geo/shared-types'
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { articlesApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
|
||||
type RegeneratableArticle = Pick<ArticleListItem | RecentArticle, 'id'>
|
||||
|
||||
export function useArticleRegenerateAction() {
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useI18n()
|
||||
const regeneratingArticleId = ref<number | null>(null)
|
||||
|
||||
const regenerateMutation = useMutation({
|
||||
mutationFn: (article: RegeneratableArticle) => articlesApi.regenerate(article.id),
|
||||
onSuccess: async () => {
|
||||
message.success(t('common.regenerateSuccess'))
|
||||
regeneratingArticleId.value = null
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['articles'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
|
||||
])
|
||||
},
|
||||
onError: (error) => {
|
||||
regeneratingArticleId.value = null
|
||||
message.error(formatError(error) || t('common.regenerateError'))
|
||||
},
|
||||
})
|
||||
|
||||
async function handleRegenerate(article: RegeneratableArticle): Promise<void> {
|
||||
regeneratingArticleId.value = article.id
|
||||
await regenerateMutation.mutateAsync(article)
|
||||
}
|
||||
|
||||
return {
|
||||
regeneratingArticleId,
|
||||
regenerateMutation,
|
||||
handleRegenerate,
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import ArticleSourceMeta from '@/components/ArticleSourceMeta.vue'
|
||||
import PublishArticleModal from '@/components/PublishArticleModal.vue'
|
||||
import { articlesApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { getPublishStatusMeta } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
|
||||
@@ -101,6 +102,8 @@ const createMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const { regeneratingArticleId, handleRegenerate } = useArticleRegenerateAction()
|
||||
|
||||
const publishStatusOptions = computed(() => [
|
||||
{ label: getPublishStatusMeta('unpublished').label, value: 'unpublished' },
|
||||
{ label: getPublishStatusMeta('publishing').label, value: 'publishing' },
|
||||
@@ -309,13 +312,16 @@ async function copyArticle(articleId: number): Promise<void> {
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<ArticleActionGroup
|
||||
v-bind="resolveArticleActionState(record.generate_status)"
|
||||
v-bind="resolveArticleActionState(record)"
|
||||
:delete-confirm-title="t('freeCreate.list.deleteConfirm')"
|
||||
:delete-loading="deleteMutation.isPending.value"
|
||||
:regenerate-confirm-title="t('common.regenerateConfirm')"
|
||||
:regenerate-loading="regeneratingArticleId === record.id"
|
||||
@publish="openPublish(record)"
|
||||
@edit="openEditor(record)"
|
||||
@preview="openPreview(record)"
|
||||
@copy="copyArticle(record.id)"
|
||||
@regenerate="handleRegenerate(record)"
|
||||
@delete="handleDelete(record.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import ArticleSourceMeta from '@/components/ArticleSourceMeta.vue'
|
||||
import PublishArticleModal from '@/components/PublishArticleModal.vue'
|
||||
import { articlesApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { getGenerateStatusMeta, getPublishStatusMeta } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
|
||||
@@ -99,6 +100,8 @@ const deleteMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const { regeneratingArticleId, handleRegenerate } = useArticleRegenerateAction()
|
||||
|
||||
const generateStatusOptions = computed(() => [
|
||||
{ label: getGenerateStatusMeta('generating').label, value: 'generating' },
|
||||
{ label: getGenerateStatusMeta('completed').label, value: 'completed' },
|
||||
@@ -435,13 +438,16 @@ async function copyArticle(articleId: number): Promise<void> {
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<ArticleActionGroup
|
||||
v-bind="resolveArticleActionState(record.generate_status)"
|
||||
v-bind="resolveArticleActionState(record)"
|
||||
:delete-confirm-title="t('imitation.list.deleteConfirm')"
|
||||
:delete-loading="deleteMutation.isPending.value"
|
||||
:regenerate-confirm-title="t('common.regenerateConfirm')"
|
||||
:regenerate-loading="regeneratingArticleId === record.id"
|
||||
@publish="openPublish(record)"
|
||||
@edit="openEditor(record)"
|
||||
@preview="openPreview(record)"
|
||||
@copy="copyArticle(record.id)"
|
||||
@regenerate="handleRegenerate(record)"
|
||||
@delete="handleDelete(record.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -34,6 +34,7 @@ import ArticleSourceMeta from '@/components/ArticleSourceMeta.vue'
|
||||
import PublishArticleModal from '@/components/PublishArticleModal.vue'
|
||||
import { articlesApi, templatesApi, workspaceApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import {
|
||||
formatDateTime,
|
||||
getGenerateStatusMeta,
|
||||
@@ -216,6 +217,8 @@ const deleteMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const { regeneratingArticleId, handleRegenerate } = useArticleRegenerateAction()
|
||||
|
||||
const templateFilterOptions = computed(() => {
|
||||
return [
|
||||
{ label: t('templates.sections.generalTemplates'), value: 'source:template' },
|
||||
@@ -599,13 +602,16 @@ function refreshRecords(): void {
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<ArticleActionGroup
|
||||
v-bind="resolveArticleActionState(record.generate_status)"
|
||||
v-bind="resolveArticleActionState(record)"
|
||||
:delete-confirm-title="t('templates.list.deleteConfirm')"
|
||||
:delete-loading="deleteMutation.isPending.value"
|
||||
:regenerate-confirm-title="t('common.regenerateConfirm')"
|
||||
:regenerate-loading="regeneratingArticleId === record.id"
|
||||
@publish="openPublish(record)"
|
||||
@edit="openEditor(record)"
|
||||
@preview="openPreview(record)"
|
||||
@copy="copyArticle(record.id)"
|
||||
@regenerate="handleRegenerate(record)"
|
||||
@delete="handleDelete(record.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -28,6 +28,7 @@ import ArticleSourceMeta from '@/components/ArticleSourceMeta.vue'
|
||||
import PublishArticleModal from '@/components/PublishArticleModal.vue'
|
||||
import { articlesApi, tenantAccountsApi, workspaceApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { formatDateTime, getTemplateMeta } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { isMediaPublishAccount } from '@/lib/publish-platforms'
|
||||
@@ -157,6 +158,8 @@ const deleteMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const { regeneratingArticleId, handleRegenerate } = useArticleRegenerateAction()
|
||||
|
||||
// Helper for the gradient classes based on index/type
|
||||
function getTemplateCardClass(idx: number): string {
|
||||
const classes = ['card-yellow', 'card-blue-light', 'card-blue-deep', 'card-grey']
|
||||
@@ -451,13 +454,16 @@ function stopRecentArticlesPolling(): void {
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<ArticleActionGroup
|
||||
v-bind="resolveArticleActionState(record.generate_status)"
|
||||
v-bind="resolveArticleActionState(record)"
|
||||
:delete-confirm-title="t('templates.list.deleteConfirm')"
|
||||
:delete-loading="deleteMutation.isPending.value"
|
||||
:regenerate-confirm-title="t('common.regenerateConfirm')"
|
||||
:regenerate-loading="regeneratingArticleId === record.id"
|
||||
@publish="openPublish(record.id)"
|
||||
@edit="openEditor(record)"
|
||||
@preview="openArticle(record.id)"
|
||||
@copy="copyArticle(record.id)"
|
||||
@regenerate="handleRegenerate(record)"
|
||||
@delete="handleDelete(record.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user