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) <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const articleDrawerOpen = ref(false);
|
||||
const selectedPublishArticleId = ref<number | null>(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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -289,7 +340,7 @@ function refreshDashboard(): void {
|
||||
v-for="card in sortedKolCards"
|
||||
:key="card.subscription_prompt_id"
|
||||
class="kol-card"
|
||||
@click="router.push(`/kol/generate/${card.subscription_prompt_id}`)"
|
||||
@click="openRefinedTemplate(card)"
|
||||
>
|
||||
<div class="kol-card-cover" :style="card.package_cover ? { backgroundImage: `url(${card.package_cover})` } : {}">
|
||||
<div v-if="!card.package_cover" class="kol-card-cover-fallback">
|
||||
|
||||
@@ -257,6 +257,9 @@ func (s *KolGenerationService) Submit(ctx context.Context, actor auth.Actor, sub
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50103, "kol_generation_article_create_failed", err.Error())
|
||||
}
|
||||
if err := articleTx.UpdateArticleGenerateStatus(ctx, articleID, actor.TenantID, "generating"); err != nil {
|
||||
return nil, response.ErrInternal(50112, "kol_generation_article_state_update_failed", err.Error())
|
||||
}
|
||||
|
||||
if err := quotaTx.UpdateQuotaReservationResource(ctx, reservationID, actor.TenantID, articleID); err != nil {
|
||||
return nil, response.ErrInternal(50104, "kol_generation_reservation_update_failed", err.Error())
|
||||
|
||||
Reference in New Issue
Block a user