+
+
+
![]()
+
+
+
+ {{ asset.type }}
-
-
-
-
- {{ asset.name }}
-
-
-
-
{{ formatDate(asset.created_at) }}
+
+
+
+
+
+
+
- 🔗 Linked
+ class="text-[10px] bg-emerald-500/20 text-emerald-400 px-2 py-1 rounded-full border border-emerald-500/20"
+ v-tooltip.left="'Linked to Character'">
+ Linked
+
+
+
+
+
+
+
+
+
+
-
diff --git a/src/views/FlexibleGenerationView.vue b/src/views/FlexibleGenerationView.vue
index 6ec0cf3..c0d083f 100644
--- a/src/views/FlexibleGenerationView.vue
+++ b/src/views/FlexibleGenerationView.vue
@@ -52,11 +52,7 @@ const historyRows = ref(50)
const historyFirst = ref(0)
const isSettingsVisible = ref(false)
-const isGenerating = ref(false)
-const generationStatus = ref('')
-const generationProgress = ref(0)
-const generationError = ref(null)
-const generatedResult = ref(null) // For immediate feedback if needed
+const isSubmitting = ref(false)
const activeOverlayId = ref(null) // For mobile tap-to-show overlay
// Options
@@ -148,6 +144,32 @@ const loadData = async () => {
if (historyRes && historyRes.generations) {
historyGenerations.value = historyRes.generations
historyTotal.value = historyRes.total_count || 0
+
+ // Resume polling for unfinished generations
+ // Resume polling for unfinished generations
+ const threeMinutesAgo = Date.now() - 3 * 60 * 1000
+
+ historyGenerations.value.forEach(gen => {
+ const status = gen.status ? gen.status.toLowerCase() : ''
+ const isActive = ['processing', 'starting', 'running'].includes(status)
+
+ let isRecent = true
+ if (gen.created_at) {
+ // Force UTC if missing timezone info (simple heuristic)
+ let timeStr = gen.created_at
+ if (timeStr.indexOf('Z') === -1 && timeStr.indexOf('+') === -1) {
+ timeStr += 'Z'
+ }
+ const createdTime = new Date(timeStr).getTime()
+ if (!isNaN(createdTime)) {
+ isRecent = createdTime > threeMinutesAgo
+ }
+ }
+
+ if (isActive && isRecent) {
+ pollGeneration(gen.id)
+ }
+ })
} else {
historyGenerations.value = Array.isArray(historyRes) ? historyRes : []
historyTotal.value = historyGenerations.value.length
@@ -183,7 +205,12 @@ const refreshHistory = async () => {
const existingIndex = historyGenerations.value.findIndex(g => g.id === gen.id)
if (existingIndex !== -1) {
// Update existing item in place to preserve state/reactivity
- Object.assign(historyGenerations.value[existingIndex], gen)
+ // Only update if not currently polling to avoid race conditions,
+ // or just update fields that might change
+ const existing = historyGenerations.value[existingIndex]
+ if (!['processing', 'starting', 'running'].includes(existing.status)) {
+ Object.assign(existing, gen)
+ }
} else {
newGenerations.push(gen)
}
@@ -202,7 +229,6 @@ const refreshHistory = async () => {
}
-
// --- Generation ---
const handleGenerate = async () => {
if (!prompt.value.trim()) return
@@ -212,10 +238,7 @@ const handleGenerate = async () => {
return
}
- isGenerating.value = true
- generationError.value = null
- generationStatus.value = 'starting'
- generationProgress.value = 0
+ isSubmitting.value = true
// Close settings to show gallery/progress (optional preference)
// isSettingsVisible.value = false
@@ -234,51 +257,123 @@ const handleGenerate = async () => {
const response = await aiService.runGeneration(payload)
if (response && response.id) {
- pollStatus(response.id)
- } else {
- // Immediate result
- isGenerating.value = false
- loadHistory() // Refresh gallery
+ // Create optimistic generation items
+ // If response is the full generation object, use it.
+ // If it's just { id: '...' }, create a placeholder.
+ // aiService.runGeneration returns response.data.
+
+ const newGen = {
+ id: response.id,
+ prompt: prompt.value,
+ status: 'starting',
+ created_at: new Date().toISOString(),
+ // Add other fields as necessary for display
+ }
+
+ // Add to history immediately
+ historyGenerations.value.unshift(newGen)
+ historyTotal.value++
+
+ // Start polling
+ pollGeneration(response.id)
+
+ // Clear prompt if desired, or keep for reuse
+ // prompt.value = ''
}
} catch (e) {
console.error('Generation failed', e)
- generationError.value = e.message || 'Generation failed'
- isGenerating.value = false
+ // Ideally show a toast error here
+ alert(e.message || 'Generation failed to start')
+ } finally {
+ isSubmitting.value = false
}
}
-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
+ const genIndex = historyGenerations.value.findIndex(g => g.id === id)
+ if (genIndex === -1) return // Should not happen if we just added it
+
+ const gen = historyGenerations.value[genIndex]
+
+ while (!completed) {
try {
const response = await aiService.getGenerationStatus(id)
- generationStatus.value = response.status
- generationProgress.value = response.progress || 0
- if (response.status === 'done') {
- completed = true
+ // Update the object in the list
+ // We use Object.assign to keep the reactive reference valid
+ Object.assign(gen, response)
- // Refresh history to show new item without resetting list
- await refreshHistory()
- } else if (response.status === 'failed') {
+ if (response.status === 'done' || response.status === 'failed') {
completed = true
- generationError.value = response.failed_reason || 'Generation failed'
- throw new Error(generationError.value)
} else {
+ // Exponential backoff or fixed interval
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 > 3) {
+ completed = true
+ gen.status = 'failed'
+ gen.failed_reason = 'Polling connection lost'
+ }
+ await new Promise(resolve => setTimeout(resolve, 5000))
}
}
- isGenerating.value = false
+}
+
+// --- Infinite Scroll ---
+const isHistoryLoading = ref(false)
+const infiniteScrollTrigger = ref(null)
+let observer = null
+
+const setupInfiniteScroll = () => {
+ observer = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && !isHistoryLoading.value && historyGenerations.value.length < historyTotal.value) {
+ loadMoreHistory()
+ }
+ }, {
+ root: null,
+ rootMargin: '100px',
+ threshold: 0.1
+ })
+
+ if (infiniteScrollTrigger.value) {
+ observer.observe(infiniteScrollTrigger.value)
+ }
+}
+
+const loadMoreHistory = async () => {
+ if (isHistoryLoading.value) return
+ isHistoryLoading.value = true
+
+ try {
+ const nextOffset = historyGenerations.value.length
+ const response = await aiService.getGenerations(historyRows.value, nextOffset)
+
+ if (response && response.generations) {
+ const newGenerations = response.generations.filter(gen =>
+ !historyGenerations.value.some(existing => existing.id === gen.id)
+ )
+ historyGenerations.value.push(...newGenerations)
+ historyTotal.value = response.total_count || historyTotal.value
+ }
+ } catch (e) {
+ console.error('Failed to load more history', e)
+ } finally {
+ isHistoryLoading.value = false
+ }
}
// --- Initial Load ---
onMounted(() => {
- loadData()
+ loadData().then(() => {
+ // slight delay to allow DOM render
+ setTimeout(setupInfiniteScroll, 500)
+ })
isSettingsVisible.value = true
})
@@ -444,6 +539,34 @@ watch(assetPickerTab, () => {
}
})
+// --- Asset Upload in Picker ---
+const assetPickerFileInput = ref(null)
+
+const triggerAssetPickerUpload = () => {
+ assetPickerFileInput.value?.click()
+}
+
+const handleAssetPickerUpload = async (event) => {
+ const target = event.target
+ if (target.files && target.files[0]) {
+ const file = target.files[0]
+ try {
+ isModalLoading.value = true
+ await dataService.uploadAsset(file)
+
+ // Switch tab to 'uploaded' and reload
+ assetPickerTab.value = 'uploaded'
+ loadModalAssets()
+
+ } catch (e) {
+ console.error('Failed to upload asset', e)
+ } finally {
+ isModalLoading.value = false
+ target.value = ''
+ }
+ }
+}
+
// --- Album Picker Logic ---
const openAlbumPicker = (gen) => {
generationToAdd.value = gen
@@ -475,6 +598,7 @@ const confirmAddToAlbum = async () => {
Gallery
+
History
@@ -507,10 +631,24 @@ const confirmAddToAlbum = async () => {
v-tooltip.top="gen.failed_reason">{{ gen.failed_reason }}
-
-
Creating...
+
+
+
+
+
+
+
+
+
+
+
{{ gen.status
+ }}...
+
{{
+ gen.progress }}%
@@ -526,6 +664,14 @@ const confirmAddToAlbum = async () => {
class="!w-6 !h-6 !rounded-full !bg-red-500/20 !border-none !text-red-400 text-[10px] hover:!bg-red-500 hover:!text-white"
@click.stop="deleteGeneration(gen)" />
+
+ {{
+ gen.cost }} $
+ {{
+ gen.execution_time_seconds.toFixed(1) }}s
+
+
-
Your creations will appear here
+
+
+
+
+
+ All items loaded
+
+