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 { computed, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
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 { message } from "ant-design-vue";
|
||||||
import { LeftOutlined } from "@ant-design/icons-vue";
|
import { LeftOutlined } from "@ant-design/icons-vue";
|
||||||
import { kolGenerateApi } from "@/lib/api";
|
import { kolGenerateApi } from "@/lib/api";
|
||||||
@@ -14,12 +14,32 @@ import {
|
|||||||
isKolVariableValueEmpty,
|
isKolVariableValueEmpty,
|
||||||
normalizeKolVariableDefinition,
|
normalizeKolVariableDefinition,
|
||||||
} from "@/lib/kol-placeholders";
|
} from "@/lib/kol-placeholders";
|
||||||
|
import { formatError } from "@/lib/errors";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const subscriptionPromptId = computed(() => String(route.params.subscriptionPromptId));
|
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({
|
const schemaQuery = useQuery({
|
||||||
queryKey: ["kol", "subscription-prompt", "schema", subscriptionPromptId],
|
queryKey: ["kol", "subscription-prompt", "schema", subscriptionPromptId],
|
||||||
@@ -64,17 +84,23 @@ const generateMutation = useMutation({
|
|||||||
enable_web_search: allowWebSearch.value ? enableWebSearch.value : undefined,
|
enable_web_search: allowWebSearch.value ? enableWebSearch.value : undefined,
|
||||||
knowledge_group_ids: allowUserKnowledge.value ? selectedKnowledgeGroupIds.value : undefined,
|
knowledge_group_ids: allowUserKnowledge.value ? selectedKnowledgeGroupIds.value : undefined,
|
||||||
}),
|
}),
|
||||||
onSuccess: (data) => {
|
onSuccess: async () => {
|
||||||
message.success(t("common.copySuccess")); // Or a generic success message
|
await Promise.all([
|
||||||
void router.push({ name: "article-editor", params: { id: String(data.article_id) } });
|
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["workspace"] }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
message.info(t("templates.wizard.messages.queued"));
|
||||||
|
|
||||||
|
await router.replace(resolveReturnRoute());
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
message.error(error.message || t("common.noData"));
|
message.error(formatError(error) || t("common.noData"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.back();
|
void router.push(resolveReturnRoute());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ function handleGenerate(promptId: number) {
|
|||||||
(sp) => sp.prompt_id === promptId && sp.package_id === packageId.value
|
(sp) => sp.prompt_id === promptId && sp.package_id === packageId.value
|
||||||
);
|
);
|
||||||
if (subPrompt) {
|
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;
|
let articlePollingTimer: number | null = null;
|
||||||
|
|
||||||
|
function hasActiveGenerationStatus(status?: string | null): boolean {
|
||||||
|
return status === "queued" || status === "pending" || status === "generating" || status === "running";
|
||||||
|
}
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (articleId: number) => articlesApi.remove(articleId),
|
mutationFn: (articleId: number) => articlesApi.remove(articleId),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -324,7 +328,11 @@ function startTemplate(template: TemplateListItem): void {
|
|||||||
function openRefinedTemplate(card: KolWorkspaceCard): void {
|
function openRefinedTemplate(card: KolWorkspaceCard): void {
|
||||||
pickerOpen.value = false;
|
pickerOpen.value = false;
|
||||||
pickerMode.value = null;
|
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> {
|
async function openEditor(article: ArticleListItem): Promise<void> {
|
||||||
@@ -362,7 +370,7 @@ watch(
|
|||||||
() => displayedArticles.value,
|
() => displayedArticles.value,
|
||||||
(items: ArticleListItem[]) => {
|
(items: ArticleListItem[]) => {
|
||||||
const hasGeneratingArticles = items.some((item: ArticleListItem) =>
|
const hasGeneratingArticles = items.some((item: ArticleListItem) =>
|
||||||
item.generate_status === "generating" || item.generate_status === "running",
|
hasActiveGenerationStatus(item.generate_status),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasGeneratingArticles) {
|
if (hasGeneratingArticles) {
|
||||||
@@ -390,8 +398,8 @@ function startArticlePolling(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
articlePollingTimer = window.setInterval(() => {
|
articlePollingTimer = window.setInterval(() => {
|
||||||
void articleListQuery.refetch();
|
void Promise.all([articleListQuery.refetch(), recentArticlesQuery.refetch()]);
|
||||||
}, 3000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopArticlePolling(): void {
|
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 type { KolWorkspaceCard, RecentArticle, TemplateCard } from "@geo/shared-types";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
import type { TableColumnsType } 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 { useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ const selectedArticleId = ref<number | null>(null);
|
|||||||
const articleDrawerOpen = ref(false);
|
const articleDrawerOpen = ref(false);
|
||||||
const selectedPublishArticleId = ref<number | null>(null);
|
const selectedPublishArticleId = ref<number | null>(null);
|
||||||
const publishModalOpen = ref(false);
|
const publishModalOpen = ref(false);
|
||||||
|
let recentArticlesPollingTimer: number | null = null;
|
||||||
|
|
||||||
const overviewQuery = useQuery({
|
const overviewQuery = useQuery({
|
||||||
queryKey: ["workspace", "overview"],
|
queryKey: ["workspace", "overview"],
|
||||||
@@ -56,6 +57,10 @@ const kolCardsQuery = useQuery({
|
|||||||
queryFn: () => workspaceApi.kolCards(),
|
queryFn: () => workspaceApi.kolCards(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function hasActiveGenerationStatus(status?: string | null): boolean {
|
||||||
|
return status === "queued" || status === "pending" || status === "generating" || status === "running";
|
||||||
|
}
|
||||||
|
|
||||||
function toTimeValue(value?: string | null): number {
|
function toTimeValue(value?: string | null): number {
|
||||||
const parsed = Date.parse(value ?? "");
|
const parsed = Date.parse(value ?? "");
|
||||||
return Number.isNaN(parsed) ? 0 : parsed;
|
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 {
|
function openArticle(articleId: number): void {
|
||||||
selectedArticleId.value = articleId;
|
selectedArticleId.value = articleId;
|
||||||
articleDrawerOpen.value = true;
|
articleDrawerOpen.value = true;
|
||||||
@@ -223,6 +236,44 @@ function refreshDashboard(): void {
|
|||||||
kolCardsQuery.refetch(),
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -289,7 +340,7 @@ function refreshDashboard(): void {
|
|||||||
v-for="card in sortedKolCards"
|
v-for="card in sortedKolCards"
|
||||||
:key="card.subscription_prompt_id"
|
:key="card.subscription_prompt_id"
|
||||||
class="kol-card"
|
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 class="kol-card-cover" :style="card.package_cover ? { backgroundImage: `url(${card.package_cover})` } : {}">
|
||||||
<div v-if="!card.package_cover" class="kol-card-cover-fallback">
|
<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 {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50103, "kol_generation_article_create_failed", err.Error())
|
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 {
|
if err := quotaTx.UpdateQuotaReservationResource(ctx, reservationID, actor.TenantID, articleID); err != nil {
|
||||||
return nil, response.ErrInternal(50104, "kol_generation_reservation_update_failed", err.Error())
|
return nil, response.ErrInternal(50104, "kol_generation_reservation_update_failed", err.Error())
|
||||||
|
|||||||
Reference in New Issue
Block a user