From 9a9d50a90099fda9326c28d58556b04814f7b520 Mon Sep 17 00:00:00 2001 From: xds Date: Mon, 16 Feb 2026 16:35:29 +0300 Subject: [PATCH] feat: Implement multi-generation support with individual status tracking and history grouping. --- package-lock.json | 21 +- package.json | 3 +- src/views/CharacterDetailView.vue | 377 +++++++++---- src/views/IdeaDetailView.vue | 861 +++++++++++++++++++++--------- src/views/IdeasView.vue | 4 +- 5 files changed, 911 insertions(+), 355 deletions(-) diff --git a/package-lock.json b/package-lock.json index adfb0ab..a4faf7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "primeicons": "^7.0.0", "primevue": "^4.5.4", "vue": "^3.5.27", - "vue-router": "^5.0.1" + "vue-router": "^5.0.1", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", @@ -6780,6 +6781,12 @@ "dev": true, "license": "MIT" }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -7812,6 +7819,18 @@ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "license": "MIT" }, + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "license": "MIT", + "dependencies": { + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index f9f430b..aa7833a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "primeicons": "^7.0.0", "primevue": "^4.5.4", "vue": "^3.5.27", - "vue-router": "^5.0.1" + "vue-router": "^5.0.1", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", diff --git a/src/views/CharacterDetailView.vue b/src/views/CharacterDetailView.vue index c8d79df..389bbe1 100644 --- a/src/views/CharacterDetailView.vue +++ b/src/views/CharacterDetailView.vue @@ -94,6 +94,40 @@ const handleDownloadResults = () => { } } +const generationCount = ref(1) + +const groupedHistoryGenerations = computed(() => { + const groups = new Map() + const result = [] + for (const gen of historyGenerations.value) { + if (gen.generation_group_id) { + if (groups.has(gen.generation_group_id)) { + groups.get(gen.generation_group_id).children.push(gen) + } else { + const group = { + id: gen.generation_group_id, + generation_group_id: gen.generation_group_id, + prompt: gen.prompt, + created_at: gen.created_at, + isGroup: true, + children: [gen], + // Use first child status for group status if needed, or derived + status: gen.status + } + groups.set(gen.generation_group_id, group) + result.push(group) + } + } else { + result.push(gen) + } + } + return result +}) + +const hasActiveGeneration = computed(() => { + return isGenerating.value || historyGenerations.value.some(g => ['starting', 'processing', 'running'].includes(g.status)) +}) + const loadData = async () => { loading.value = true const charId = route.params.id @@ -116,6 +150,13 @@ const loadData = async () => { if (historyResponse && historyResponse.generations) { historyGenerations.value = historyResponse.generations historyTotal.value = historyResponse.total_count || 0 + + // Resume polling for active generations + historyGenerations.value.forEach(gen => { + if (['starting', 'processing', 'running'].includes(gen.status)) { + pollGeneration(gen.id) + } + }) } else { historyGenerations.value = Array.isArray(historyResponse) ? historyResponse : [] historyTotal.value = historyGenerations.value.length @@ -158,6 +199,13 @@ const loadHistory = async () => { if (response && response.generations) { historyGenerations.value = response.generations historyTotal.value = response.total_count || 0 + + // Resume polling for newly loaded active generations if any + historyGenerations.value.forEach(gen => { + if (['starting', 'processing', 'running'].includes(gen.status)) { + pollGeneration(gen.id) + } + }) } else { historyGenerations.value = Array.isArray(response) ? response : [] historyTotal.value = historyGenerations.value.length @@ -306,50 +354,79 @@ const onModalAssetsPage = (event) => { loadAllAssets() } -const pollStatus = async (id) => { +const pollGeneration = async (id) => { let completed = false - while (!completed && isGenerating.value) { + let attempts = 0 + + // Find the generation object in our list to update it specifically + // We poll and update the object in historyGenerations + + while (!completed) { + // Check if generation is still needed to be polled (e.g. if we navigated away? logic handles this by default) + const genIndex = historyGenerations.value.findIndex(g => g.id === id) + if (genIndex === -1) { + // Maybe it's a new one not yet in history list? + // Logic in handleGenerate adds it first, so it should be there. + // If not found, maybe stop polling? + if (attempts > 5) return // Stop if keep failing to find it + } + + const gen = historyGenerations.value[genIndex] + try { const response = await aiService.getGenerationStatus(id) - generationStatus.value = response.status - generationProgress.value = response.progress || 0 + + if (gen) { + Object.assign(gen, response) + // Update specific legacy ref if this is the latest one user is looking at in the "Creating..." block + if (isGenerating.value && generatedResult.value === null) { + generationStatus.value = response.status + generationProgress.value = response.progress || 0 + } + } if (response.status === 'done') { completed = true - generationSuccess.value = true - - // Refresh assets list - const assets = await loadAssets() - - // Display created assets from the list (without selecting them) - if (response.assets_list && response.assets_list.length > 0) { - const resultAssets = assets.filter(a => response.assets_list.includes(a.id)) - generatedResult.value = { - type: 'assets', - assets: resultAssets, - tech_prompt: response.tech_prompt, - execution_time: response.execution_time_seconds, - api_execution_time: response.api_execution_time_seconds, - token_usage: response.token_usage - } + if (isGenerating.value && (!generatedResult.value || generatedResult.value === id)) { + // only finish the "generating" blocking state if *all* active ones are done? + // No, simpler: isGenerating tracks the *submission* process mainly, + // but we also use it to show the big spinner. + // If we support multiple, we should probably stop showing the big spinner + // once submission is done and just show history status. + } + + // If we want to show the result of the *just finished* one in the big box: + if (isGenerating.value) { + // logic for "main" result display } - loadHistory() } else if (response.status === 'failed') { completed = true - generationError.value = response.failed_reason || 'Generation failed on server' - throw new Error(generationError.value) + if (gen) gen.failed_reason = response.failed_reason } else { - // Wait before next poll await new Promise(resolve => setTimeout(resolve, 2000)) } } catch (e) { - console.error('Polling failed', e) - completed = true - isGenerating.value = false + console.error(`Polling failed for ${id}`, e) + attempts++ + if (attempts > 10) { + completed = true + if (gen) { + gen.status = 'failed' + gen.failed_reason = 'Polling connection lost' + } + } + await new Promise(resolve => setTimeout(resolve, 5000)) } } - isGenerating.value = false + + // Check if we can turn off global loading + // Actually, let's turn off "isGenerating" immediately after submission success, + // and let the history handle the progress. + // BUT user wants to see it in the main slot? + // "display 4 copies in one slot in the feed" + // "track status of generations after sending" + // "block sending next if active" } const restoreGeneration = async (gen) => { @@ -495,6 +572,7 @@ const handleGenerate = async () => { try { if (sendToTelegram.value && !telegramId.value) { alert("Please enter your Telegram ID") + isGenerating.value = false return } @@ -510,27 +588,55 @@ const handleGenerate = async () => { prompt: prompt.value, assets_list: selectedAssets.value.map(a => a.id), telegram_id: sendToTelegram.value ? telegramId.value : null, - use_profile_image: useProfileImage.value + use_profile_image: useProfileImage.value, + count: generationCount.value } const response = await aiService.runGeneration(payload) - // response is expected to have an 'id' for the generation task - if (response && response.id) { - pollStatus(response.id) + + let generations = [] + if (response && response.generations) { + generations = response.generations + } else if (Array.isArray(response)) { + generations = response } else { - // Fallback if it returns data immediately - generatedResult.value = response - generationSuccess.value = true - isGenerating.value = false + generations = [response] } + + // Add to history and start polling + for (const gen of generations) { + if (gen && gen.id) { + const newGen = { + ...gen, + status: gen.status || 'starting', + created_at: new Date().toISOString() + } + historyGenerations.value.unshift(newGen) + historyTotal.value++ + + pollGeneration(gen.id) + } + } + + // We set isGenerating to false immediately after successful submission + // because we want detailed status to be tracked in the history list. + // However, if we want to block the UI, hasActiveGeneration will do that. + // But for the 'big spinner', if we want to show it, we can keep it for a bit or just rely on history. + // User requested: "block next if active". + // Let's reset isGenerating so the big spinner goes away, and the user sees the progress in the history list. + isGenerating.value = false prompt.value = '' + + // Scroll to history? + // Maybe open/switch history tab or ensure it is visible? + // For now, let's just let the user see it in the history section. + } catch (e) { console.error('Generation failed', e) isGenerating.value = false + generationError.value = e.message || 'Failed to start generation' } } - -