likes
This commit is contained in:
@@ -190,7 +190,7 @@ const navItems = computed(() => {
|
|||||||
|
|
||||||
<!-- Mobile Bottom Nav -->
|
<!-- Mobile Bottom Nav -->
|
||||||
<nav
|
<nav
|
||||||
class="md:hidden fixed bottom-0 left-0 right-0 h-12 bg-slate-900/90 backdrop-blur-xl border-t border-white/10 z-50 flex justify-around items-center px-2">
|
class="md:hidden fixed bottom-0 left-0 right-0 h-16 pb-4 bg-slate-900/90 backdrop-blur-xl border-t border-white/10 z-50 flex justify-around items-center px-2">
|
||||||
<div v-for="item in navItems" :key="item.path" :class="[
|
<div v-for="item in navItems" :key="item.path" :class="[
|
||||||
'flex flex-col items-center p-1.5 rounded-lg transition-all',
|
'flex flex-col items-center p-1.5 rounded-lg transition-all',
|
||||||
isActive(item.path)
|
isActive(item.path)
|
||||||
|
|||||||
@@ -12,6 +12,25 @@ export const dataService = {
|
|||||||
const response = await api.get(`/assets/${id}`)
|
const response = await api.get(`/assets/${id}`)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAssetMetadata: async (id) => {
|
||||||
|
const response = await api.head(`/assets/${id}`)
|
||||||
|
return {
|
||||||
|
content_type: response.headers['content-type'],
|
||||||
|
content_length: response.headers['content-length']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadAsset: async (id) => {
|
||||||
|
// Return full response to access headers
|
||||||
|
const response = await api.get(`/assets/${id}`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
'Range': 'bytes=0-' // Explicitly request full content to play nice with backend streaming
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
getCharacters: async () => {
|
getCharacters: async () => {
|
||||||
// Spec says /api/characters/ (with trailing slash) but usually client shouldn't matter too much if config is good,
|
// Spec says /api/characters/ (with trailing slash) but usually client shouldn't matter too much if config is good,
|
||||||
|
|||||||
10
src/services/inspirationService.js
Normal file
10
src/services/inspirationService.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import api from './api';
|
||||||
|
|
||||||
|
export const inspirationService = {
|
||||||
|
getInspirations: (limit = 20, offset = 0) => api.get('/inspirations', { params: { limit, offset } }),
|
||||||
|
getInspiration: (id) => api.get(`/inspirations/${id}`),
|
||||||
|
createInspiration: (data) => api.post('/inspirations', data),
|
||||||
|
updateInspiration: (id, data) => api.put(`/inspirations/${id}`, data),
|
||||||
|
deleteInspiration: (id) => api.delete(`/inspirations/${id}`),
|
||||||
|
completeInspiration: (id, is_completed = true) => api.patch(`/inspirations/${id}/complete`, null, { params: { is_completed } })
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { ideaService } from '../services/ideaService';
|
import { ideaService } from '../services/ideaService';
|
||||||
|
import { inspirationService } from '../services/inspirationService';
|
||||||
|
|
||||||
export const useIdeaStore = defineStore('ideas', () => {
|
export const useIdeaStore = defineStore('ideas', () => {
|
||||||
const ideas = ref([]);
|
const ideas = ref([]);
|
||||||
|
const inspirations = ref([]);
|
||||||
const currentIdea = ref(null);
|
const currentIdea = ref(null);
|
||||||
|
const currentInspiration = ref(null); // New state
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
const totalIdeas = ref(0);
|
const totalIdeas = ref(0);
|
||||||
@@ -61,6 +64,25 @@ export const useIdeaStore = defineStore('ideas', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New action to fetch a single inspiration
|
||||||
|
async function fetchInspiration(id) {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
currentInspiration.value = null;
|
||||||
|
try {
|
||||||
|
const response = await inspirationService.getInspiration(id);
|
||||||
|
currentInspiration.value = response.data;
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching inspiration:', err);
|
||||||
|
error.value = err.response?.data?.detail || 'Failed to fetch inspiration';
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function updateIdea(id, data) {
|
async function updateIdea(id, data) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
@@ -143,9 +165,85 @@ export const useIdeaStore = defineStore('ideas', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Inspirations ---
|
||||||
|
async function fetchInspirations(limit = 20, offset = 0) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await inspirationService.getInspirations(limit, offset);
|
||||||
|
inspirations.value = response.data.inspirations || response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching inspirations:', err);
|
||||||
|
error.value = err.response?.data?.detail || 'Failed to fetch inspirations';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createInspiration(data) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await inspirationService.createInspiration(data);
|
||||||
|
await fetchInspirations();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating inspiration:', err);
|
||||||
|
error.value = err.response?.data?.detail || 'Failed to create inspiration';
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateInspiration(id, data) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await inspirationService.updateInspiration(id, data);
|
||||||
|
await fetchInspirations();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating inspiration:', err);
|
||||||
|
error.value = err.response?.data?.detail || 'Failed to update inspiration';
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteInspiration(id) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await inspirationService.deleteInspiration(id);
|
||||||
|
await fetchInspirations();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting inspiration:', err);
|
||||||
|
error.value = err.response?.data?.detail || 'Failed to delete inspiration';
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeInspiration(id) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await inspirationService.completeInspiration(id);
|
||||||
|
await fetchInspirations();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error completing inspiration:', err);
|
||||||
|
error.value = err.response?.data?.detail || 'Failed to complete inspiration';
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ideas,
|
ideas,
|
||||||
|
inspirations,
|
||||||
currentIdea,
|
currentIdea,
|
||||||
|
currentInspiration,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
totalIdeas,
|
totalIdeas,
|
||||||
@@ -156,6 +254,12 @@ export const useIdeaStore = defineStore('ideas', () => {
|
|||||||
deleteIdea,
|
deleteIdea,
|
||||||
addGenerationToIdea,
|
addGenerationToIdea,
|
||||||
removeGenerationFromIdea,
|
removeGenerationFromIdea,
|
||||||
fetchIdeaGenerations
|
fetchIdeaGenerations,
|
||||||
|
fetchInspirations,
|
||||||
|
createInspiration,
|
||||||
|
updateInspiration,
|
||||||
|
deleteInspiration,
|
||||||
|
completeInspiration,
|
||||||
|
fetchInspiration
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ const router = useRouter()
|
|||||||
const ideaStore = useIdeaStore()
|
const ideaStore = useIdeaStore()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { currentIdea, loading, error } = storeToRefs(ideaStore)
|
const { currentIdea, currentInspiration, loading, error } = storeToRefs(ideaStore)
|
||||||
const generations = ref([])
|
const generations = ref([])
|
||||||
|
const inspirationContentType = ref('')
|
||||||
|
|
||||||
// --- Idea Name Editing ---
|
// --- Idea Name Editing ---
|
||||||
const isEditingName = ref(false)
|
const isEditingName = ref(false)
|
||||||
@@ -218,6 +219,11 @@ onMounted(async () => {
|
|||||||
])
|
])
|
||||||
console.log('Fetched idea:', currentIdea.value)
|
console.log('Fetched idea:', currentIdea.value)
|
||||||
if (currentIdea.value) {
|
if (currentIdea.value) {
|
||||||
|
// Check for inspiration
|
||||||
|
if (currentIdea.value.inspiration_id) {
|
||||||
|
ideaStore.fetchInspiration(currentIdea.value.inspiration_id)
|
||||||
|
}
|
||||||
|
|
||||||
// Check for autostart query param
|
// Check for autostart query param
|
||||||
if (route.query.autostart === 'true') {
|
if (route.query.autostart === 'true') {
|
||||||
// Slight delay to ensure everything is reactive and mounted
|
// Slight delay to ensure everything is reactive and mounted
|
||||||
@@ -256,7 +262,7 @@ const loadCharacters = async () => {
|
|||||||
const fetchGenerations = async (ideaId) => {
|
const fetchGenerations = async (ideaId) => {
|
||||||
loadingGenerations.value = true
|
loadingGenerations.value = true
|
||||||
try {
|
try {
|
||||||
const response = await ideaStore.fetchIdeaGenerations(ideaId, 100, onlyLiked.value)
|
const response = await ideaStore.fetchIdeaGenerations(ideaId, 100, 0, onlyLiked.value)
|
||||||
let loadedGens = []
|
let loadedGens = []
|
||||||
if (response.data && response.data.generations) {
|
if (response.data && response.data.generations) {
|
||||||
loadedGens = response.data.generations
|
loadedGens = response.data.generations
|
||||||
@@ -611,6 +617,25 @@ const openImagePreview = (imageList, startIdx = 0) => {
|
|||||||
isImagePreviewVisible.value = true
|
isImagePreviewVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Inspiration Logic ---
|
||||||
|
const showInspirationDialog = ref(false)
|
||||||
|
|
||||||
|
const openInspirationDialog = () => {
|
||||||
|
showInspirationDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentInspiration, async (newVal) => {
|
||||||
|
if (newVal && newVal.asset_id) {
|
||||||
|
try {
|
||||||
|
const meta = await dataService.getAssetMetadata(newVal.asset_id)
|
||||||
|
inspirationContentType.value = meta.content_type
|
||||||
|
} catch (e) {
|
||||||
|
// console.warn('Failed to get asset metadata', e)
|
||||||
|
inspirationContentType.value = 'image/jpeg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// --- Computeds ---
|
// --- Computeds ---
|
||||||
const groupedGenerations = computed(() => {
|
const groupedGenerations = computed(() => {
|
||||||
// Group by generation_group_id if present
|
// Group by generation_group_id if present
|
||||||
@@ -976,6 +1001,11 @@ watch(viewMode, (v) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button v-if="currentInspiration" icon="pi pi-lightbulb" text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !text-yellow-400 hover:!bg-yellow-400/10"
|
||||||
|
v-tooltip.bottom="'View Inspiration'"
|
||||||
|
@click="openInspirationDialog" />
|
||||||
|
|
||||||
<Button :icon="onlyLiked ? 'pi pi-heart-fill' : 'pi pi-heart'"
|
<Button :icon="onlyLiked ? 'pi pi-heart-fill' : 'pi pi-heart'"
|
||||||
@click="onlyLiked = !onlyLiked" rounded text
|
@click="onlyLiked = !onlyLiked" rounded text
|
||||||
class="!w-7 !h-7 !p-0"
|
class="!w-7 !h-7 !p-0"
|
||||||
@@ -1523,6 +1553,44 @@ watch(viewMode, (v) => {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Inspiration Dialog -->
|
||||||
|
<Dialog v-model:visible="showInspirationDialog" modal header="Inspiration"
|
||||||
|
:style="{ width: '90vw', maxWidth: '1000px', height: '90vh' }"
|
||||||
|
:pt="{ root: { class: '!bg-slate-900 !border !border-white/10 flex flex-col' }, header: { class: '!bg-slate-900 !border-b !border-white/5 !text-white flex-shrink-0' }, content: { class: '!bg-slate-900 !p-4 flex-1 overflow-hidden flex flex-col' } }">
|
||||||
|
<div v-if="currentInspiration" class="flex flex-col gap-4 h-full">
|
||||||
|
<!-- Asset Viewer -->
|
||||||
|
<div v-if="currentInspiration.asset_id" class="flex-1 min-h-0 rounded-xl overflow-hidden border border-white/10 bg-black/20 flex items-center justify-center">
|
||||||
|
<video v-if="inspirationContentType && inspirationContentType.startsWith('video/')"
|
||||||
|
:src="API_URL + '/assets/' + currentInspiration.asset_id"
|
||||||
|
controls autoplay loop muted
|
||||||
|
class="max-w-full max-h-full object-contain" />
|
||||||
|
<img v-else
|
||||||
|
:src="API_URL + '/assets/' + currentInspiration.asset_id"
|
||||||
|
class="max-w-full max-h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Caption -->
|
||||||
|
<div v-if="currentInspiration.caption" class="flex flex-col gap-2 flex-shrink-0">
|
||||||
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Caption</label>
|
||||||
|
<div class="max-h-[150px] overflow-y-auto custom-scrollbar bg-slate-800/50 p-3 rounded-lg border border-white/5">
|
||||||
|
<p class="text-sm text-slate-200 whitespace-pre-wrap">{{ currentInspiration.caption }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex justify-end mt-2 flex-shrink-0 gap-2">
|
||||||
|
<Button label="Close" text @click="showInspirationDialog = false" class="!text-slate-400 hover:!text-white" />
|
||||||
|
<a v-if="currentInspiration.source_url" :href="currentInspiration.source_url" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button label="Go to Source" icon="pi pi-external-link"
|
||||||
|
class="!bg-violet-600 !border-none hover:!bg-violet-500" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex justify-center py-8">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useIdeaStore } from '../stores/ideas'
|
import { useIdeaStore } from '../stores/ideas'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { dataService } from '../services/dataService' // Added import
|
import { dataService } from '../services/dataService'
|
||||||
import Skeleton from 'primevue/skeleton'
|
import Skeleton from 'primevue/skeleton'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
@@ -15,12 +15,12 @@ import Checkbox from 'primevue/checkbox'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const ideaStore = useIdeaStore()
|
const ideaStore = useIdeaStore()
|
||||||
const { ideas, loading } = storeToRefs(ideaStore)
|
const { ideas, loading, inspirations } = storeToRefs(ideaStore)
|
||||||
|
|
||||||
const showCreateDialog = ref(false)
|
const showCreateDialog = ref(false)
|
||||||
const newIdea = ref({ name: '', description: '' })
|
const newIdea = ref({ name: '', description: '' })
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const API_URL = import.meta.env.VITE_API_URL
|
const API_URL = import.meta.env.VITE_API_URL || '/api'
|
||||||
const isSettingsVisible = ref(localStorage.getItem('ideas_view_settings_visible') !== 'false')
|
const isSettingsVisible = ref(localStorage.getItem('ideas_view_settings_visible') !== 'false')
|
||||||
|
|
||||||
watch(isSettingsVisible, (val) => {
|
watch(isSettingsVisible, (val) => {
|
||||||
@@ -56,6 +56,135 @@ const handleRename = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Inspirations Logic ---
|
||||||
|
const showInspirationDialog = ref(false)
|
||||||
|
const newInspirationLink = ref('')
|
||||||
|
const creatingInspiration = ref(false)
|
||||||
|
const downloadingInspirations = ref(new Set())
|
||||||
|
|
||||||
|
// Preview Logic
|
||||||
|
const showPreviewDialog = ref(false)
|
||||||
|
const previewInspiration = ref(null)
|
||||||
|
const previewAsset = ref(null)
|
||||||
|
const loadingPreview = ref(false)
|
||||||
|
|
||||||
|
const openPreview = async (insp) => {
|
||||||
|
if (!insp || !insp.asset_id) return
|
||||||
|
previewInspiration.value = insp
|
||||||
|
showPreviewDialog.value = true
|
||||||
|
loadingPreview.value = true
|
||||||
|
previewAsset.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use HEAD request to get content-type without downloading body
|
||||||
|
previewAsset.value = await dataService.getAssetMetadata(insp.asset_id)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load asset metadata', e)
|
||||||
|
// Fallback: assume image if metadata fails, or let user try
|
||||||
|
previewAsset.value = { content_type: 'image/jpeg' }
|
||||||
|
} finally {
|
||||||
|
loadingPreview.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIdeaInspiration = (idea) => {
|
||||||
|
if (idea.inspiration) return idea.inspiration
|
||||||
|
if (idea.inspiration_id) {
|
||||||
|
return inspirations.value.find(i => i.id === idea.inspiration_id)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateInspiration = async () => {
|
||||||
|
if (!newInspirationLink.value) return
|
||||||
|
creatingInspiration.value = true
|
||||||
|
try {
|
||||||
|
await ideaStore.createInspiration({ source_url: newInspirationLink.value })
|
||||||
|
showInspirationDialog.value = false
|
||||||
|
newInspirationLink.value = ''
|
||||||
|
} finally {
|
||||||
|
creatingInspiration.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteInspiration = async (id) => {
|
||||||
|
if (confirm('Are you sure you want to delete this inspiration?')) {
|
||||||
|
await ideaStore.deleteInspiration(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCompleteInspiration = async (id) => {
|
||||||
|
await ideaStore.completeInspiration(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadInspiration = async (insp) => {
|
||||||
|
if (!insp.asset_id) return
|
||||||
|
|
||||||
|
downloadingInspirations.value.add(insp.id)
|
||||||
|
try {
|
||||||
|
// Use dataService which uses axios with correct headers (Auth, X-Project-ID)
|
||||||
|
// Now returns full response object
|
||||||
|
const response = await dataService.downloadAsset(insp.asset_id)
|
||||||
|
const blob = response.data
|
||||||
|
|
||||||
|
// Determine extension
|
||||||
|
let ext = 'jpg'
|
||||||
|
if (blob.type === 'video/mp4') ext = 'mp4'
|
||||||
|
else if (blob.type === 'image/png') ext = 'png'
|
||||||
|
else if (blob.type === 'image/jpeg') ext = 'jpg'
|
||||||
|
else if (blob.type === 'image/webp') ext = 'webp'
|
||||||
|
|
||||||
|
const fileName = `inspiration-${insp.id.substring(0, 8)}.${ext}`
|
||||||
|
const file = new File([blob], fileName, { type: blob.type })
|
||||||
|
|
||||||
|
// Check if mobile device
|
||||||
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||||
|
|
||||||
|
if (isMobile && navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||||
|
try {
|
||||||
|
await navigator.share({
|
||||||
|
files: [file],
|
||||||
|
title: 'Inspiration',
|
||||||
|
text: insp.caption || 'Check out this inspiration'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
console.error('Share failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Desktop download (or fallback if share not supported)
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Download failed', e)
|
||||||
|
// Try to extract a meaningful error message
|
||||||
|
let msg = 'Failed to download asset'
|
||||||
|
if (e.response) {
|
||||||
|
msg += `: ${e.response.status} ${e.response.statusText}`
|
||||||
|
} else if (e.message) {
|
||||||
|
msg += `: ${e.message}`
|
||||||
|
}
|
||||||
|
alert(msg)
|
||||||
|
} finally {
|
||||||
|
downloadingInspirations.value.delete(insp.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIdeaFromInspiration = (inspiration) => {
|
||||||
|
selectedInspiration.value = inspiration
|
||||||
|
isSettingsVisible.value = true
|
||||||
|
// Optionally scroll to settings or highlight it
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Generation Settings ---
|
// --- Generation Settings ---
|
||||||
const prompt = ref('')
|
const prompt = ref('')
|
||||||
const selectedModel = ref('flux-schnell')
|
const selectedModel = ref('flux-schnell')
|
||||||
@@ -67,6 +196,7 @@ const telegramId = ref('')
|
|||||||
const useProfileImage = ref(true)
|
const useProfileImage = ref(true)
|
||||||
const useEnvironment = ref(false)
|
const useEnvironment = ref(false)
|
||||||
const isSubmittingGen = ref(false)
|
const isSubmittingGen = ref(false)
|
||||||
|
const selectedInspiration = ref(null) // New state for selected inspiration
|
||||||
|
|
||||||
// Character & Assets
|
// Character & Assets
|
||||||
const characters = ref([])
|
const characters = ref([])
|
||||||
@@ -144,6 +274,7 @@ const restoreSettings = () => {
|
|||||||
name: 'Asset ' + id.substring(0, 4)
|
name: 'Asset ' + id.substring(0, 4)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
// Note: We don't restore selectedInspiration to avoid stale state, or we could if needed.
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to restore settings', e)
|
console.error('Failed to restore settings', e)
|
||||||
}
|
}
|
||||||
@@ -174,6 +305,7 @@ onMounted(async () => {
|
|||||||
restoreSettings()
|
restoreSettings()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ideaStore.fetchIdeas(),
|
ideaStore.fetchIdeas(),
|
||||||
|
ideaStore.fetchInspirations(),
|
||||||
loadCharacters()
|
loadCharacters()
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -215,16 +347,26 @@ const handleGenerate = async () => {
|
|||||||
try {
|
try {
|
||||||
// 1. Create a random name idea
|
// 1. Create a random name idea
|
||||||
const randomName = 'Session ' + new Date().toLocaleString()
|
const randomName = 'Session ' + new Date().toLocaleString()
|
||||||
const newIdea = await ideaStore.createIdea({
|
const ideaData = {
|
||||||
name: randomName,
|
name: randomName,
|
||||||
description: 'Auto-generated from quick start'
|
description: 'Auto-generated from quick start'
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Pass inspiration ID if selected
|
||||||
|
if (selectedInspiration.value) {
|
||||||
|
ideaData.inspiration_id = selectedInspiration.value.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const newIdea = await ideaStore.createIdea(ideaData)
|
||||||
|
|
||||||
// 2. Save settings (already handled by watch, but ensure latest)
|
// 2. Save settings (already handled by watch, but ensure latest)
|
||||||
saveSettings()
|
saveSettings()
|
||||||
|
|
||||||
// 3. Navigate with autostart param
|
// 3. Navigate with autostart param
|
||||||
router.push({ path: `/ideas/${newIdea.id}`, query: { autostart: 'true' } })
|
router.push({ path: `/ideas/${newIdea.id}`, query: { autostart: 'true' } })
|
||||||
|
|
||||||
|
// Clear inspiration after use
|
||||||
|
selectedInspiration.value = null
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to start session', e)
|
console.error('Failed to start session', e)
|
||||||
@@ -325,78 +467,159 @@ const handleAssetPickerUpload = async (event) => {
|
|||||||
<div class="flex flex-col h-full font-sans relative">
|
<div class="flex flex-col h-full font-sans relative">
|
||||||
<!-- Content Area (Scrollable) -->
|
<!-- Content Area (Scrollable) -->
|
||||||
<div class="flex-1 overflow-y-auto p-4 md:p-6 pb-48 custom-scrollbar">
|
<div class="flex-1 overflow-y-auto p-4 md:p-6 pb-48 custom-scrollbar">
|
||||||
<!-- Top Bar -->
|
|
||||||
<header class="flex justify-between items-center gap-0 mb-3 border-b border-white/5 pb-2">
|
<div class="flex flex-col lg:flex-row gap-6">
|
||||||
<div class="flex flex-row items-center justify-between gap-2">
|
<!-- LEFT COLUMN: Ideas List -->
|
||||||
<h1 class="text-lg font-bold !m-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
|
<div class="flex-1 flex flex-col gap-4">
|
||||||
Ideas</h1>
|
<!-- Top Bar -->
|
||||||
<p class="mt-0.5 mb-0 text-[10px] text-slate-500">Your creative sessions</p>
|
<header class="flex justify-between items-center gap-0 border-b border-white/5 pb-2">
|
||||||
</div>
|
<div class="flex flex-row items-center justify-between gap-2">
|
||||||
<!-- REMOVED NEW IDEA BUTTON -->
|
<h1 class="text-lg font-bold !m-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
|
||||||
</header>
|
Ideas</h1>
|
||||||
|
<p class="mt-0.5 mb-0 text-[10px] text-slate-500">Your creative sessions</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading && ideas.length === 0" class="flex flex-col gap-4">
|
<div v-if="loading && ideas.length === 0" class="flex flex-col gap-4">
|
||||||
<div v-for="i in 6" :key="i" class="glass-panel rounded-2xl p-4 flex gap-4 items-center">
|
<div v-for="i in 6" :key="i" class="glass-panel rounded-2xl p-4 flex gap-4 items-center">
|
||||||
<Skeleton width="4rem" height="4rem" class="rounded-lg" />
|
<Skeleton width="4rem" height="4rem" class="rounded-lg" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Skeleton width="40%" height="1.5rem" class="mb-2" />
|
<Skeleton width="40%" height="1.5rem" class="mb-2" />
|
||||||
<Skeleton width="60%" height="1rem" />
|
<Skeleton width="60%" height="1rem" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-else-if="ideas.length === 0"
|
|
||||||
class="flex flex-col items-center justify-center h-96 text-slate-400 bg-slate-900/30 rounded-3xl border border-dashed border-white/10">
|
|
||||||
<div class="w-20 h-20 rounded-full bg-violet-500/10 flex items-center justify-center mb-6">
|
|
||||||
<i class="pi pi-lightbulb text-4xl text-violet-400"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold text-white mb-2">No ideas yet</h3>
|
|
||||||
<p class="text-slate-400 mb-6 max-w-sm text-center">Use the panel below to start a new creative session.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ideas List (Vertical) -->
|
|
||||||
<div v-else class="flex flex-col gap-3">
|
|
||||||
<div v-for="idea in ideas" :key="idea.id"
|
|
||||||
class="glass-panel rounded-xl p-3 flex gap-4 items-center cursor-pointer border border-white/5 hover:bg-slate-800/80 hover:border-violet-500/30 group transition-all"
|
|
||||||
@click="goToDetail(idea.id)">
|
|
||||||
|
|
||||||
<!-- Thumbnail -->
|
|
||||||
<div
|
|
||||||
class="w-16 h-16 rounded-lg bg-slate-800 flex-shrink-0 overflow-hidden relative border border-white/10">
|
|
||||||
<img v-if="idea.last_generation && idea.last_generation.status == 'done' && idea.last_generation.result_list.length > 0"
|
|
||||||
:src="API_URL + '/assets/' + idea.last_generation.result_list[0] + '?thumbnail=true'"
|
|
||||||
class="w-full h-full object-cover" />
|
|
||||||
<div v-else class="w-full h-full flex items-center justify-center bg-slate-800 text-slate-600">
|
|
||||||
<i class="pi pi-image text-xl"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Details -->
|
<!-- Empty State -->
|
||||||
<div class="flex-1 min-w-0">
|
<div v-else-if="ideas.length === 0"
|
||||||
<div class="flex items-center gap-2">
|
class="flex flex-col items-center justify-center h-96 text-slate-400 bg-slate-900/30 rounded-3xl border border-dashed border-white/10">
|
||||||
<h3
|
<div class="w-20 h-20 rounded-full bg-violet-500/10 flex items-center justify-center mb-6">
|
||||||
class="m-0 text-lg font-bold text-slate-200 group-hover:text-violet-300 transition-colors truncate">
|
<i class="pi pi-lightbulb text-4xl text-violet-400"></i>
|
||||||
{{ idea.name }}</h3>
|
|
||||||
<Button icon="pi pi-pencil" text rounded size="small"
|
|
||||||
class="!w-6 !h-6 !text-slate-500 hover:!text-violet-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
@click.stop="openRenameDialog(idea)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0 text-sm text-slate-500 truncate">{{ idea.description || 'No description' }}</p>
|
<h3 class="text-xl font-semibold text-white mb-2">No ideas yet</h3>
|
||||||
|
<p class="text-slate-400 mb-6 max-w-sm text-center">Use the panel below to start a new creative session.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Meta -->
|
<!-- Ideas List (Vertical) -->
|
||||||
<div class="flex items-center gap-4 text-xs text-slate-500">
|
<div v-else class="flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-1 bg-slate-900/50 px-2 py-1 rounded-md">
|
<div v-for="idea in ideas" :key="idea.id"
|
||||||
<i class="pi pi-images text-[10px]"></i>
|
class="glass-panel rounded-xl p-3 flex gap-4 items-center cursor-pointer border border-white/5 hover:bg-slate-800/80 hover:border-violet-500/30 group transition-all"
|
||||||
<span>{{ idea.generation_ids?.length || 0 }}</span>
|
@click="goToDetail(idea.id)">
|
||||||
|
|
||||||
|
<!-- Thumbnail -->
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 rounded-lg bg-slate-800 flex-shrink-0 overflow-hidden relative border border-white/10">
|
||||||
|
<img v-if="idea.last_generation && idea.last_generation.status == 'done' && idea.last_generation.result_list.length > 0"
|
||||||
|
:src="API_URL + '/assets/' + idea.last_generation.result_list[0] + '?thumbnail=true'"
|
||||||
|
class="w-full h-full object-cover" />
|
||||||
|
<div v-else class="w-full h-full flex items-center justify-center bg-slate-800 text-slate-600">
|
||||||
|
<i class="pi pi-image text-xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3
|
||||||
|
class="m-0 text-lg font-bold text-slate-200 group-hover:text-violet-300 transition-colors truncate">
|
||||||
|
{{ idea.name }}</h3>
|
||||||
|
<Button icon="pi pi-pencil" text rounded size="small"
|
||||||
|
class="!w-6 !h-6 !text-slate-500 hover:!text-violet-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
@click.stop="openRenameDialog(idea)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 text-sm text-slate-500 truncate">{{ idea.description || 'No description' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meta -->
|
||||||
|
<div class="flex items-center gap-4 text-xs text-slate-500">
|
||||||
|
<div v-if="getIdeaInspiration(idea)"
|
||||||
|
class="flex items-center gap-1 bg-violet-500/20 border border-violet-500/30 px-2 py-1 rounded-md text-violet-300 hover:bg-violet-500/30 transition-colors cursor-pointer"
|
||||||
|
@click.stop="openPreview(getIdeaInspiration(idea))">
|
||||||
|
<i class="pi pi-bolt text-[10px]"></i>
|
||||||
|
<span class="hidden sm:inline">Inspiration</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 bg-slate-900/50 px-2 py-1 rounded-md">
|
||||||
|
<i class="pi pi-images text-[10px]"></i>
|
||||||
|
<span>{{ idea.generation_ids?.length || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-chevron-right text-slate-600 group-hover:text-white transition-colors"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<i class="pi pi-chevron-right text-slate-600 group-hover:text-white transition-colors"></i>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT COLUMN: Inspirations -->
|
||||||
|
<div class="w-full lg:w-80 flex flex-col gap-4">
|
||||||
|
<header class="flex justify-between items-center gap-0 border-b border-white/5 pb-2">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<h2 class="text-lg font-bold !m-0 text-slate-200">Inspirations</h2>
|
||||||
|
</div>
|
||||||
|
<Button icon="pi pi-plus" size="small" rounded text
|
||||||
|
class="!w-8 !h-8 !text-violet-400 hover:!bg-violet-500/10"
|
||||||
|
@click="showInspirationDialog = true" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="inspirations.length === 0" class="flex flex-col items-center justify-center py-10 text-slate-500 bg-slate-900/20 rounded-xl border border-dashed border-white/5">
|
||||||
|
<i class="pi pi-bolt text-2xl mb-2 opacity-50"></i>
|
||||||
|
<p class="text-xs">No inspirations yet</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col gap-3">
|
||||||
|
<div v-for="insp in inspirations" :key="insp.id"
|
||||||
|
class="glass-panel rounded-xl p-3 flex flex-col gap-2 border border-white/5 hover:border-white/10 transition-all relative group"
|
||||||
|
:class="{'opacity-50': insp.is_completed}">
|
||||||
|
|
||||||
|
<!-- Content Preview (Text Only) -->
|
||||||
|
<div class="text-xs text-slate-300 break-all line-clamp-3 mb-1">
|
||||||
|
{{ insp.caption || insp.source_url }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Source Link -->
|
||||||
|
<div v-if="insp.source_url" class="flex items-center gap-1 mb-1">
|
||||||
|
<a :href="insp.source_url" target="_blank" class="text-[10px] text-violet-400 hover:underline truncate max-w-full flex items-center gap-1">
|
||||||
|
<i class="pi pi-external-link text-[8px]"></i>
|
||||||
|
Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex justify-end gap-1 mt-1 border-t border-white/5 pt-2">
|
||||||
|
<Button v-if="insp.asset_id"
|
||||||
|
icon="pi pi-eye"
|
||||||
|
text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !text-slate-500 hover:!text-violet-400"
|
||||||
|
@click="openPreview(insp)"
|
||||||
|
v-tooltip="'View Content'" />
|
||||||
|
|
||||||
|
<Button v-if="insp.asset_id"
|
||||||
|
:icon="downloadingInspirations.has(insp.id) ? 'pi pi-spin pi-spinner' : 'pi pi-download'"
|
||||||
|
text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !text-slate-500 hover:!text-blue-400"
|
||||||
|
@click="handleDownloadInspiration(insp)"
|
||||||
|
v-tooltip="'Download'" />
|
||||||
|
|
||||||
|
<Button icon="pi pi-trash" text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !text-slate-500 hover:!text-red-400"
|
||||||
|
@click="handleDeleteInspiration(insp.id)"
|
||||||
|
v-tooltip="'Delete'" />
|
||||||
|
|
||||||
|
<Button v-if="!insp.is_completed" icon="pi pi-check" text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !text-slate-500 hover:!text-green-400"
|
||||||
|
@click="handleCompleteInspiration(insp.id)"
|
||||||
|
v-tooltip="'Mark Complete'" />
|
||||||
|
|
||||||
|
<Button v-if="!insp.is_completed" icon="pi pi-sparkles" text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !text-violet-400 hover:!bg-violet-500/20"
|
||||||
|
@click="startIdeaFromInspiration(insp)"
|
||||||
|
v-tooltip="'Create Idea'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,6 +639,45 @@ const handleAssetPickerUpload = async (event) => {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Create Inspiration Dialog -->
|
||||||
|
<Dialog v-model:visible="showInspirationDialog" header="Add Inspiration" modal :style="{ width: '400px' }"
|
||||||
|
:pt="{ root: { class: '!bg-slate-800 !border-white/10' }, header: { class: '!bg-slate-800' }, content: { class: '!bg-slate-800' } }">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Instagram Link / Content</label>
|
||||||
|
<InputText v-model="newInspirationLink" placeholder="https://instagram.com/..." class="w-full !bg-slate-700 !border-white/10 !text-white" autofocus @keyup.enter="handleCreateInspiration" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<Button label="Cancel" text @click="showInspirationDialog = false" class="!text-slate-400 hover:!text-white" />
|
||||||
|
<Button label="Add" :loading="creatingInspiration" @click="handleCreateInspiration" class="!bg-violet-600 !border-none hover:!bg-violet-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Preview Dialog -->
|
||||||
|
<Dialog v-model:visible="showPreviewDialog" modal :header="previewInspiration?.caption || 'Preview'"
|
||||||
|
:style="{ width: '90vw', maxWidth: '800px' }"
|
||||||
|
:pt="{ root: { class: '!bg-slate-900 !border !border-white/10' }, header: { class: '!bg-slate-900 !border-b !border-white/5 !text-white' }, content: { class: '!bg-slate-900 !p-0' }, closeButton: { class: '!text-slate-400 hover:!text-white' } }">
|
||||||
|
<div class="flex flex-col items-center justify-center p-4 min-h-[300px]">
|
||||||
|
<div v-if="loadingPreview" class="flex flex-col items-center gap-2">
|
||||||
|
<i class="pi pi-spin pi-spinner text-violet-500 text-2xl"></i>
|
||||||
|
<span class="text-slate-400 text-sm">Loading content...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="previewAsset" class="w-full h-full flex items-center justify-center">
|
||||||
|
<video v-if="previewAsset.content_type?.startsWith('video/')"
|
||||||
|
:src="API_URL + '/assets/' + previewInspiration.asset_id"
|
||||||
|
controls autoplay class="max-w-full max-h-[70vh] rounded-lg shadow-2xl">
|
||||||
|
</video>
|
||||||
|
<img v-else
|
||||||
|
:src="API_URL + '/assets/' + previewInspiration.asset_id"
|
||||||
|
class="max-w-full max-h-[70vh] rounded-lg shadow-2xl object-contain" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-slate-500">
|
||||||
|
Failed to load content
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<!-- SETTINGS PANEL (Bottom - Persistent) -->
|
<!-- SETTINGS PANEL (Bottom - Persistent) -->
|
||||||
<div v-if="isSettingsVisible"
|
<div v-if="isSettingsVisible"
|
||||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 w-[95%] max-w-4xl glass-panel border border-white/10 bg-slate-900/95 backdrop-blur-xl px-4 py-3 z-[60] !rounded-[2rem] shadow-2xl flex flex-col gap-1 max-h-[60vh] overflow-y-auto">
|
class="absolute bottom-4 left-1/2 -translate-x-1/2 w-[95%] max-w-4xl glass-panel border border-white/10 bg-slate-900/95 backdrop-blur-xl px-4 py-3 z-[60] !rounded-[2rem] shadow-2xl flex flex-col gap-1 max-h-[60vh] overflow-y-auto">
|
||||||
@@ -424,6 +686,13 @@ const handleAssetPickerUpload = async (event) => {
|
|||||||
<div class="w-12 h-1 bg-white/20 rounded-full hover:bg-white/40 transition-colors"></div>
|
<div class="w-12 h-1 bg-white/20 rounded-full hover:bg-white/40 transition-colors"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Inspiration Indicator -->
|
||||||
|
<div v-if="selectedInspiration" class="flex items-center gap-2 bg-violet-500/20 border border-violet-500/30 rounded-lg px-3 py-2 mb-2 animate-in fade-in slide-in-from-bottom-2">
|
||||||
|
<i class="pi pi-bolt text-violet-400 text-sm"></i>
|
||||||
|
<span class="text-xs text-violet-200 truncate flex-1">Using inspiration: {{ selectedInspiration.caption || selectedInspiration.source_url }}</span>
|
||||||
|
<Button icon="pi pi-times" text rounded size="small" class="!w-5 !h-5 !text-violet-300 hover:!text-white" @click="selectedInspiration = null" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col lg:flex-row gap-3">
|
<div class="flex flex-col lg:flex-row gap-3">
|
||||||
<!-- LEFT COLUMN: Prompt + Character + Assets -->
|
<!-- LEFT COLUMN: Prompt + Character + Assets -->
|
||||||
<div class="flex-1 flex flex-col gap-2">
|
<div class="flex-1 flex flex-col gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user