From 6e77f72c53fd52d761426c16f42fb2bf86829323 Mon Sep 17 00:00:00 2001 From: liangxu Date: Sat, 18 Apr 2026 17:17:17 +0800 Subject: [PATCH] feat(kol): return to source page after generation and poll while busy - Add a ?source=workspace|templates query param when opening the generate page so the back button and the post-submit redirect return to the originating page; invalidate articles + workspace caches on success so the new article shows up immediately. - Poll recent articles every 10s in WorkspaceView and TemplatesView while any article is in queued/pending/generating/running, and stop the timer once nothing is active. - Mark the new article as generating inside the generation Submit transaction so the client sees the right status as soon as the task is queued. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/admin-web/src/views/KolGenerateView.vue | 38 +++++++++++-- .../src/views/KolPackageDetailView.vue | 6 +- apps/admin-web/src/views/TemplatesView.vue | 16 ++++-- apps/admin-web/src/views/WorkspaceView.vue | 55 ++++++++++++++++++- .../tenant/app/kol_generation_service.go | 3 + 5 files changed, 105 insertions(+), 13 deletions(-) diff --git a/apps/admin-web/src/views/KolGenerateView.vue b/apps/admin-web/src/views/KolGenerateView.vue index 9af8837..e30fafb 100644 --- a/apps/admin-web/src/views/KolGenerateView.vue +++ b/apps/admin-web/src/views/KolGenerateView.vue @@ -2,7 +2,7 @@ import { computed, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; -import { useMutation, useQuery } from "@tanstack/vue-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query"; import { message } from "ant-design-vue"; import { LeftOutlined } from "@ant-design/icons-vue"; import { kolGenerateApi } from "@/lib/api"; @@ -14,12 +14,32 @@ import { isKolVariableValueEmpty, normalizeKolVariableDefinition, } from "@/lib/kol-placeholders"; +import { formatError } from "@/lib/errors"; const route = useRoute(); const router = useRouter(); +const queryClient = useQueryClient(); const { t } = useI18n(); const subscriptionPromptId = computed(() => String(route.params.subscriptionPromptId)); +const sourcePage = computed<"workspace" | "templates" | null>(() => { + if (route.query.source === "workspace") { + return "workspace"; + } + if (route.query.source === "templates") { + return "templates"; + } + return null; +}); + +function resolveReturnRoute(): + | { name: "workspace" } + | { name: "articles-templates" } { + if (sourcePage.value === "workspace") { + return { name: "workspace" }; + } + return { name: "articles-templates" }; +} const schemaQuery = useQuery({ queryKey: ["kol", "subscription-prompt", "schema", subscriptionPromptId], @@ -64,17 +84,23 @@ const generateMutation = useMutation({ enable_web_search: allowWebSearch.value ? enableWebSearch.value : undefined, knowledge_group_ids: allowUserKnowledge.value ? selectedKnowledgeGroupIds.value : undefined, }), - onSuccess: (data) => { - message.success(t("common.copySuccess")); // Or a generic success message - void router.push({ name: "article-editor", params: { id: String(data.article_id) } }); + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["articles"] }), + queryClient.invalidateQueries({ queryKey: ["workspace"] }), + ]); + + message.info(t("templates.wizard.messages.queued")); + + await router.replace(resolveReturnRoute()); }, onError: (error: any) => { - message.error(error.message || t("common.noData")); + message.error(formatError(error) || t("common.noData")); }, }); function handleBack() { - router.back(); + void router.push(resolveReturnRoute()); } function handleSubmit() { diff --git a/apps/admin-web/src/views/KolPackageDetailView.vue b/apps/admin-web/src/views/KolPackageDetailView.vue index c2de40c..85e0393 100644 --- a/apps/admin-web/src/views/KolPackageDetailView.vue +++ b/apps/admin-web/src/views/KolPackageDetailView.vue @@ -77,7 +77,11 @@ function handleGenerate(promptId: number) { (sp) => sp.prompt_id === promptId && sp.package_id === packageId.value ); if (subPrompt) { - void router.push({ name: "kol-generate", params: { subscriptionPromptId: String(subPrompt.id) } }); + void router.push({ + name: "kol-generate", + params: { subscriptionPromptId: String(subPrompt.id) }, + query: { source: "templates" }, + }); } } diff --git a/apps/admin-web/src/views/TemplatesView.vue b/apps/admin-web/src/views/TemplatesView.vue index 22598f7..160bf34 100644 --- a/apps/admin-web/src/views/TemplatesView.vue +++ b/apps/admin-web/src/views/TemplatesView.vue @@ -189,6 +189,10 @@ const displayedArticleTotal = computed(() => let articlePollingTimer: number | null = null; +function hasActiveGenerationStatus(status?: string | null): boolean { + return status === "queued" || status === "pending" || status === "generating" || status === "running"; +} + const deleteMutation = useMutation({ mutationFn: (articleId: number) => articlesApi.remove(articleId), onSuccess: async () => { @@ -324,7 +328,11 @@ function startTemplate(template: TemplateListItem): void { function openRefinedTemplate(card: KolWorkspaceCard): void { pickerOpen.value = false; pickerMode.value = null; - void router.push({ name: "kol-generate", params: { subscriptionPromptId: String(card.subscription_prompt_id) } }); + void router.push({ + name: "kol-generate", + params: { subscriptionPromptId: String(card.subscription_prompt_id) }, + query: { source: "templates" }, + }); } async function openEditor(article: ArticleListItem): Promise { @@ -362,7 +370,7 @@ watch( () => displayedArticles.value, (items: ArticleListItem[]) => { const hasGeneratingArticles = items.some((item: ArticleListItem) => - item.generate_status === "generating" || item.generate_status === "running", + hasActiveGenerationStatus(item.generate_status), ); if (hasGeneratingArticles) { @@ -390,8 +398,8 @@ function startArticlePolling(): void { } articlePollingTimer = window.setInterval(() => { - void articleListQuery.refetch(); - }, 3000); + void Promise.all([articleListQuery.refetch(), recentArticlesQuery.refetch()]); + }, 10000); } function stopArticlePolling(): void { diff --git a/apps/admin-web/src/views/WorkspaceView.vue b/apps/admin-web/src/views/WorkspaceView.vue index 436641b..0e5624a 100644 --- a/apps/admin-web/src/views/WorkspaceView.vue +++ b/apps/admin-web/src/views/WorkspaceView.vue @@ -16,7 +16,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query"; import type { KolWorkspaceCard, RecentArticle, TemplateCard } from "@geo/shared-types"; import { message } from "ant-design-vue"; import type { TableColumnsType } from "ant-design-vue"; -import { computed, ref } from "vue"; +import { computed, onBeforeUnmount, ref, watch } from "vue"; import { useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; @@ -38,6 +38,7 @@ const selectedArticleId = ref(null); const articleDrawerOpen = ref(false); const selectedPublishArticleId = ref(null); const publishModalOpen = ref(false); +let recentArticlesPollingTimer: number | null = null; const overviewQuery = useQuery({ queryKey: ["workspace", "overview"], @@ -56,6 +57,10 @@ const kolCardsQuery = useQuery({ queryFn: () => workspaceApi.kolCards(), }); +function hasActiveGenerationStatus(status?: string | null): boolean { + return status === "queued" || status === "pending" || status === "generating" || status === "running"; +} + function toTimeValue(value?: string | null): number { const parsed = Date.parse(value ?? ""); return Number.isNaN(parsed) ? 0 : parsed; @@ -163,6 +168,14 @@ function openTemplate(template: TemplateCard): void { }); } +function openRefinedTemplate(card: KolWorkspaceCard): void { + void router.push({ + name: "kol-generate", + params: { subscriptionPromptId: String(card.subscription_prompt_id) }, + query: { source: "workspace" }, + }); +} + function openArticle(articleId: number): void { selectedArticleId.value = articleId; articleDrawerOpen.value = true; @@ -223,6 +236,44 @@ function refreshDashboard(): void { kolCardsQuery.refetch(), ]); } + +watch( + () => recentArticlesQuery.data.value ?? [], + (items: RecentArticle[]) => { + const hasGeneratingArticles = items.some((item) => hasActiveGenerationStatus(item.generate_status)); + + if (hasGeneratingArticles) { + startRecentArticlesPolling(); + return; + } + + stopRecentArticlesPolling(); + }, + { immediate: true }, +); + +onBeforeUnmount(() => { + stopRecentArticlesPolling(); +}); + +function startRecentArticlesPolling(): void { + if (recentArticlesPollingTimer !== null) { + return; + } + + recentArticlesPollingTimer = window.setInterval(() => { + void recentArticlesQuery.refetch(); + }, 10000); +} + +function stopRecentArticlesPolling(): void { + if (recentArticlesPollingTimer === null) { + return; + } + + window.clearInterval(recentArticlesPollingTimer); + recentArticlesPollingTimer = null; +}