fixes
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.1",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.3.1",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useProjectsStore } from '@/stores/projectsStore'
|
||||
import { aiService } from '@/services/aiService'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
|
||||
@@ -13,6 +14,17 @@ const projectsStore = useProjectsStore()
|
||||
const { projects, currentProject } = storeToRefs(projectsStore)
|
||||
|
||||
const selectedProject = ref(null)
|
||||
const usageCost = ref(0)
|
||||
|
||||
const fetchUsage = async () => {
|
||||
try {
|
||||
// Fetch current context usage (user or project depending on header)
|
||||
const report = await aiService.getUsageReport()
|
||||
usageCost.value = report.summary?.total_cost || 0
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch sidebar usage", e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Ensure we have projects
|
||||
@@ -23,6 +35,7 @@ onMounted(async () => {
|
||||
if (currentProject.value) {
|
||||
selectedProject.value = currentProject.value.id
|
||||
}
|
||||
fetchUsage()
|
||||
})
|
||||
|
||||
// Watch for external changes (like selecting from the list view)
|
||||
@@ -160,6 +173,12 @@ const navItems = computed(() => {
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="flex items-center gap-4 shrink-0">
|
||||
<!-- Usage Stat -->
|
||||
<div v-if="usageCost > 0" class="hidden xl:flex flex-col items-end mr-2">
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-tighter">Usage</span>
|
||||
<span class="text-sm font-bold text-violet-400">${{ usageCost.toFixed(2) }}</span>
|
||||
</div>
|
||||
|
||||
<div @click="handleLogout"
|
||||
class="w-10 h-10 rounded-xl bg-red-500/10 text-red-400 flex items-center justify-center cursor-pointer hover:bg-red-500/20 transition-all font-bold"
|
||||
v-tooltip.bottom="'Logout'">
|
||||
|
||||
@@ -66,5 +66,22 @@ export const aiService = {
|
||||
linked_assets: linkedAssets
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Get usage statistics (runs, tokens, cost)
|
||||
async getUsageReport(breakdown = null, projectId = null) {
|
||||
const params = {}
|
||||
if (breakdown) params.breakdown = breakdown
|
||||
|
||||
const config = { params, headers: {} }
|
||||
if (projectId) {
|
||||
config.headers['X-Project-ID'] = projectId
|
||||
} else if (projectId === false) {
|
||||
// Explicitly ignore current active project header
|
||||
config.headers['X-Project-ID'] = ''
|
||||
}
|
||||
|
||||
const response = await api.get('/generations/usage', config)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { dataService } from '../services/dataService'
|
||||
import { aiService } from '../services/aiService'
|
||||
import {computed, onMounted, ref} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {dataService} from '../services/dataService'
|
||||
import {aiService} from '../services/aiService'
|
||||
import Button from 'primevue/button'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import Tag from 'primevue/tag'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import FileUpload from 'primevue/fileupload'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import Message from 'primevue/message'
|
||||
@@ -491,6 +489,15 @@ const undoImprovePrompt = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const pastePrompt = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) prompt.value = text
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard', err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reuse Logic ---
|
||||
|
||||
const reusePrompt = (gen) => {
|
||||
@@ -761,13 +768,17 @@ const handleGenerate = async () => {
|
||||
|
||||
<div class="relative w-full">
|
||||
<Textarea v-model="prompt" rows="3" autoResize placeholder="Describe..."
|
||||
class="w-full bg-slate-900 border-white/10 text-white rounded-lg p-2 focus:border-violet-500 transition-all text-xs pr-10" />
|
||||
class="w-full bg-slate-900 border-white/10 text-white rounded-lg p-2 focus:border-violet-500 transition-all text-xs pr-16" />
|
||||
|
||||
<div class="absolute top-1.5 right-1.5 flex gap-1">
|
||||
<Button v-if="previousPrompt" icon="pi pi-undo"
|
||||
class="!p-1 !w-6 !h-6 !text-[10px] bg-slate-800 hover:bg-slate-700 border-white/10 text-slate-300"
|
||||
@click="undoImprovePrompt" v-tooltip.top="'Rollback'" />
|
||||
|
||||
<Button icon="pi pi-clipboard"
|
||||
class="!p-1 !w-6 !h-6 !text-[10px] bg-slate-800 hover:bg-slate-700 border-white/10 text-slate-400"
|
||||
@click="pastePrompt" v-tooltip.top="'Paste'" />
|
||||
|
||||
<Button icon="pi pi-sparkles" :loading="isImprovingPrompt"
|
||||
:disabled="prompt.length <= 10"
|
||||
class="!p-1 !w-6 !h-6 !text-[10px] bg-violet-600/20 hover:bg-violet-600/30 border-violet-500/30 text-violet-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
|
||||
@@ -166,8 +166,13 @@ const historyTotal = ref(0)
|
||||
const historyRows = ref(50)
|
||||
const historyFirst = ref(0)
|
||||
|
||||
const isSettingsVisible = ref(false)
|
||||
const isSettingsVisible = ref(localStorage.getItem('flexible_gen_settings_visible') !== 'false')
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
watch(isSettingsVisible, (val) => {
|
||||
localStorage.setItem('flexible_gen_settings_visible', val)
|
||||
})
|
||||
|
||||
const activeOverlayId = ref(null) // For mobile tap-to-show overlay
|
||||
const filterCharacter = ref(null) // Character filter for gallery
|
||||
|
||||
@@ -671,6 +676,15 @@ const clearPrompt = () => {
|
||||
previousPrompt.value = ''
|
||||
}
|
||||
|
||||
const pastePrompt = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) prompt.value = text
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard', err)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteGeneration = async (gen) => {
|
||||
if (!gen) return
|
||||
try {
|
||||
@@ -1144,6 +1158,9 @@ const confirmAddToAlbum = async () => {
|
||||
:disabled="!prompt || prompt.length <= 10"
|
||||
class="!py-0.5 !px-2 !text-[10px] !h-6 !bg-violet-600/20 hover:!bg-violet-600/30 !border-violet-500/30 !text-violet-400 disabled:opacity-50"
|
||||
@click="handleImprovePrompt" />
|
||||
<Button icon="pi pi-clipboard" label="Paste"
|
||||
class="!py-0.5 !px-2 !text-[10px] !h-6 !bg-slate-800 hover:!bg-slate-700 !border-white/10 !text-slate-400"
|
||||
@click="pastePrompt" />
|
||||
<Button icon="pi pi-times" label="Clear"
|
||||
class="!py-0.5 !px-2 !text-[10px] !h-6 !bg-slate-800 hover:!bg-slate-700 !border-white/10 !text-slate-400"
|
||||
@click="clearPrompt" />
|
||||
|
||||
@@ -35,6 +35,40 @@ const toast = useToast()
|
||||
const { currentIdea, loading, error } = storeToRefs(ideaStore)
|
||||
const generations = ref([])
|
||||
|
||||
// --- Idea Name Editing ---
|
||||
const isEditingName = ref(false)
|
||||
const editableName = ref('')
|
||||
|
||||
const toggleEditName = () => {
|
||||
if (!currentIdea.value) return
|
||||
editableName.value = currentIdea.value.name
|
||||
isEditingName.value = true
|
||||
nextTick(() => {
|
||||
const input = document.querySelector('.idea-name-input input')
|
||||
if (input) input.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const saveName = async () => {
|
||||
if (!editableName.value.trim() || editableName.value === currentIdea.value.name) {
|
||||
isEditingName.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await ideaStore.updateIdea(currentIdea.value.id, {
|
||||
name: editableName.value.trim()
|
||||
})
|
||||
if (success) {
|
||||
toast.add({ severity: 'success', summary: 'Success', detail: 'Idea renamed', life: 2000 })
|
||||
}
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to rename idea', life: 3000 })
|
||||
} finally {
|
||||
isEditingName.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = ref('')
|
||||
const negativePrompt = ref('')
|
||||
const selectedModel = ref('flux-schnell')
|
||||
@@ -108,7 +142,11 @@ watch([prompt, quality, aspectRatio, imageCount, selectedModel, sendToTelegram,
|
||||
|
||||
const viewMode = ref('feed') // 'feed' or 'gallery'
|
||||
const isSubmitting = ref(false)
|
||||
const isSettingsVisible = ref(true)
|
||||
const isSettingsVisible = ref(localStorage.getItem('idea_detail_settings_visible') !== 'false')
|
||||
|
||||
watch(isSettingsVisible, (val) => {
|
||||
localStorage.setItem('idea_detail_settings_visible', val)
|
||||
})
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
|
||||
@@ -413,6 +451,15 @@ const clearPrompt = () => {
|
||||
previousPrompt.value = ''
|
||||
}
|
||||
|
||||
const pastePrompt = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) prompt.value = text
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Asset Picker Logic ---
|
||||
const isAssetPickerVisible = ref(false)
|
||||
@@ -832,14 +879,24 @@ watch(viewMode, (v) => {
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-slate-950/50 relative">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="h-16 border-b border-white/5 flex items-center justify-between px-6 bg-slate-900/80 backdrop-blur z-20">
|
||||
class="h-12 border-b border-white/5 flex items-center justify-between px-6 bg-slate-900/80 backdrop-blur z-20">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-lg font-bold text-slate-200 truncate max-w-[200px] md:max-w-md">{{
|
||||
currentIdea?.name || 'Loading...' }}</h1>
|
||||
<div v-if="isEditingName" class="flex items-center gap-2">
|
||||
<InputText v-model="editableName"
|
||||
class="idea-name-input !bg-slate-800 !border-violet-500/50 !text-white !py-0.5 !h-8 !text-base !font-bold"
|
||||
@keyup.enter="saveName"
|
||||
@blur="saveName"
|
||||
/>
|
||||
</div>
|
||||
<h1 v-else class="text-base font-bold text-slate-200 truncate max-w-[200px] md:max-w-md cursor-pointer hover:text-violet-400 transition-colors"
|
||||
@click="toggleEditName">
|
||||
{{ currentIdea?.name || 'Loading...' }}
|
||||
<i class="pi pi-pencil text-[9px] ml-1 opacity-50"></i>
|
||||
</h1>
|
||||
<span
|
||||
class="px-2 py-0.5 rounded-full bg-slate-800 text-[10px] text-slate-400 border border-white/5">Idea
|
||||
class="px-2 py-0.5 rounded-full bg-slate-800 text-[9px] text-slate-400 border border-white/5">Idea
|
||||
Session</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1076,6 +1133,9 @@ watch(viewMode, (v) => {
|
||||
:disabled="!prompt || prompt.length <= 10"
|
||||
class="!py-0 !px-1.5 !text-[9px] !h-5 !bg-violet-600/20 hover:!bg-violet-600/30 !border-violet-500/30 !text-violet-400 disabled:opacity-50"
|
||||
@click="handleImprovePrompt" />
|
||||
<Button icon="pi pi-clipboard" label="Paste"
|
||||
class="!py-0 !px-1.5 !text-[9px] !h-5 !bg-slate-800 hover:!bg-slate-700 !border-white/10 !text-slate-400"
|
||||
@click="pastePrompt" />
|
||||
<Button icon="pi pi-times" label="Clear"
|
||||
class="!py-0 !px-1.5 !text-[9px] !h-5 !bg-slate-800 hover:!bg-slate-700 !border-white/10 !text-slate-400"
|
||||
@click="clearPrompt" />
|
||||
|
||||
@@ -21,6 +21,40 @@ const showCreateDialog = ref(false)
|
||||
const newIdea = ref({ name: '', description: '' })
|
||||
const submitting = ref(false)
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
const isSettingsVisible = ref(localStorage.getItem('ideas_view_settings_visible') !== 'false')
|
||||
|
||||
watch(isSettingsVisible, (val) => {
|
||||
localStorage.setItem('ideas_view_settings_visible', val)
|
||||
})
|
||||
|
||||
// --- Rename Idea ---
|
||||
const showRenameDialog = ref(false)
|
||||
const ideaToRename = ref(null)
|
||||
const newName = ref('')
|
||||
const renaming = ref(false)
|
||||
|
||||
const openRenameDialog = (idea) => {
|
||||
ideaToRename.value = idea
|
||||
newName.value = idea.name
|
||||
showRenameDialog.value = true
|
||||
}
|
||||
|
||||
const handleRename = async () => {
|
||||
if (!newName.value.trim() || newName.value === ideaToRename.value.name) {
|
||||
showRenameDialog.value = false
|
||||
return
|
||||
}
|
||||
|
||||
renaming.value = true
|
||||
try {
|
||||
await ideaStore.updateIdea(ideaToRename.value.id, {
|
||||
name: newName.value.trim()
|
||||
})
|
||||
showRenameDialog.value = false
|
||||
} finally {
|
||||
renaming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Generation Settings ---
|
||||
const prompt = ref('')
|
||||
@@ -161,6 +195,15 @@ const handleGenerate = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const pastePrompt = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) prompt.value = text
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard', err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Asset Picker Logic ---
|
||||
const isAssetPickerVisible = ref(false)
|
||||
const assetPickerTab = ref('all') // 'all', 'uploaded', 'generated'
|
||||
@@ -243,14 +286,14 @@ const handleAssetPickerUpload = async (event) => {
|
||||
<template>
|
||||
<div class="flex flex-col h-full font-sans relative">
|
||||
<!-- Content Area (Scrollable) -->
|
||||
<div class="flex-1 overflow-y-auto p-8 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-end mb-8 border-b border-white/5 pb-6">
|
||||
<header class="flex justify-between items-end mb-4 border-b border-white/5 pb-4">
|
||||
<div>
|
||||
<h1
|
||||
class="text-4xl font-bold m-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
|
||||
class="text-2xl font-bold m-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
|
||||
Ideas</h1>
|
||||
<p class="mt-2 mb-0 text-slate-400">Your creative sessions and experiments</p>
|
||||
<p class="mt-1 mb-0 text-xs text-slate-400">Your creative sessions and experiments</p>
|
||||
</div>
|
||||
<!-- REMOVED NEW IDEA BUTTON -->
|
||||
</header>
|
||||
@@ -296,9 +339,15 @@ const handleAssetPickerUpload = async (event) => {
|
||||
|
||||
<!-- Details -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3
|
||||
class="m-0 text-lg font-bold text-slate-200 group-hover:text-violet-300 transition-colors truncate">
|
||||
{{ idea.name }}</h3>
|
||||
<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>
|
||||
|
||||
@@ -315,13 +364,29 @@ const handleAssetPickerUpload = async (event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Idea Dialog (Removed) -->
|
||||
|
||||
<!-- Rename Dialog -->
|
||||
<Dialog v-model:visible="showRenameDialog" header="Rename Idea" 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">New Name</label>
|
||||
<InputText v-model="newName" class="w-full !bg-slate-700 !border-white/10 !text-white" autofocus @keyup.enter="handleRename" />
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 mt-2">
|
||||
<Button label="Cancel" text @click="showRenameDialog = false" class="!text-slate-400 hover:!text-white" />
|
||||
<Button label="Save" :loading="renaming" @click="handleRename" class="!bg-violet-600 !border-none hover:!bg-violet-500" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<!-- SETTINGS PANEL (Bottom - Persistent) -->
|
||||
<div
|
||||
<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">
|
||||
|
||||
<div class="w-full flex justify-center -mt-1 mb-1 cursor-pointer" @click="isSettingsVisible = false">
|
||||
<div class="w-12 h-1 bg-white/20 rounded-full hover:bg-white/40 transition-colors"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-3">
|
||||
<!-- LEFT COLUMN: Prompt + Character + Assets -->
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
@@ -329,7 +394,14 @@ const handleAssetPickerUpload = async (event) => {
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Prompt</label>
|
||||
<!-- Optional: Add Clear/Improve buttons here if desired, keeping simple for now to match layout -->
|
||||
<div class="flex gap-1">
|
||||
<Button icon="pi pi-clipboard" label="Paste" size="small" text
|
||||
class="!text-slate-400 hover:!text-white hover:!bg-white/10 !py-0 !px-1.5 !text-[9px] !h-5"
|
||||
@click="pastePrompt" />
|
||||
<Button icon="pi pi-times" label="Clear" size="small" text
|
||||
class="!text-slate-400 hover:!text-white hover:!bg-white/10 !py-0 !px-1.5 !text-[9px] !h-5"
|
||||
@click="prompt = ''" />
|
||||
</div>
|
||||
</div>
|
||||
<Textarea v-model="prompt" rows="2"
|
||||
placeholder="Describe what you want to create... (Auto-starts new session)"
|
||||
@@ -460,6 +532,13 @@ const handleAssetPickerUpload = async (event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="!isSettingsVisible" class="absolute bottom-8 left-1/2 -translate-x-1/2 z-10">
|
||||
<Button label="Open Controls" icon="pi pi-chevron-up" @click="isSettingsVisible = true" rounded
|
||||
class="!bg-violet-600 !border-none !shadow-xl !font-bold shadow-violet-500/40 !px-6 !py-3" />
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<Dialog v-model:visible="isAssetPickerVisible" modal header="Select Assets"
|
||||
:style="{ width: '80vw', maxWidth: '900px' }"
|
||||
: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' }, footer: { class: '!bg-slate-900 !border-t !border-white/5 !p-4' }, closeButton: { class: '!text-slate-400 hover:!text-white' } }">
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { dataService } from '../services/dataService'
|
||||
import { aiService } from '../services/aiService'
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {dataService} from '../services/dataService'
|
||||
import {aiService} from '../services/aiService'
|
||||
import Button from 'primevue/button'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import Message from 'primevue/message'
|
||||
import Tag from 'primevue/tag'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Paginator from 'primevue/paginator'
|
||||
@@ -361,6 +359,15 @@ const clearPrompt = () => {
|
||||
prompt.value = ''
|
||||
}
|
||||
|
||||
const pastePrompt = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) prompt.value = text
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard', err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reuse Logic ---
|
||||
|
||||
const reusePrompt = (gen) => {
|
||||
@@ -478,6 +485,9 @@ onMounted(() => {
|
||||
:disabled="prompt.length <= 10" size="small"
|
||||
class="!py-0.5 !px-2 !text-[10px] bg-violet-600/20 hover:bg-violet-600/30 border-violet-500/30 text-violet-400 disabled:opacity-50"
|
||||
@click="handleImprovePrompt" />
|
||||
<Button icon="pi pi-clipboard" label="Paste" size="small"
|
||||
class="!py-0.5 !px-2 !text-[10px] bg-slate-800 hover:bg-slate-700 border-white/10 text-slate-400"
|
||||
@click="pastePrompt" />
|
||||
<Button icon="pi pi-times" label="Clear" size="small"
|
||||
class="!py-0.5 !px-2 !text-[10px] bg-slate-800 hover:bg-slate-700 border-white/10 text-slate-400"
|
||||
@click="clearPrompt" />
|
||||
|
||||
@@ -55,6 +55,15 @@ const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(generatedPrompt.value)
|
||||
}
|
||||
|
||||
const pastePrompt = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) userPrompt.value = text
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -124,11 +133,14 @@ const copyToClipboard = () => {
|
||||
|
||||
<!-- Optional Prompt -->
|
||||
<div class="glass-panel p-1 rounded-2xl border border-white/5">
|
||||
<div class="p-4 border-b border-white/5">
|
||||
<div class="p-4 border-b border-white/5 flex justify-between items-center">
|
||||
<label class="text-sm font-bold text-slate-300 flex items-center gap-2">
|
||||
<i class="pi pi-align-left"></i> Additional Instructions <span
|
||||
class="text-slate-500 font-normal">(Optional)</span>
|
||||
</label>
|
||||
<Button icon="pi pi-clipboard" label="Paste" size="small" text
|
||||
class="!text-slate-400 hover:!text-white hover:!bg-white/10 !py-1"
|
||||
@click="pastePrompt" />
|
||||
</div>
|
||||
<textarea v-model="userPrompt"
|
||||
class="w-full bg-transparent border-none p-4 text-slate-100 focus:outline-none focus:ring-0 placeholder-slate-600 min-h-[100px] resize-none"
|
||||
|
||||
@@ -1,92 +1,147 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 animate-fade-in" v-if="project">
|
||||
<!-- Header -->
|
||||
<div class="glass-panel p-8 rounded-xl mb-8 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 p-4 opacity-10">
|
||||
<i class="pi pi-folder text-9xl text-white"></i>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<Button icon="pi pi-arrow-left" text rounded @click="router.push('/projects')" />
|
||||
<span v-if="isCurrentProject"
|
||||
class="bg-green-500/20 text-green-400 text-xs px-2 py-1 rounded-full border border-green-500/30 font-medium">
|
||||
Active Project
|
||||
</span>
|
||||
<div class="h-full overflow-y-auto custom-scrollbar">
|
||||
<div v-if="project" class="container mx-auto p-4 md:p-8 animate-fade-in">
|
||||
<!-- Header -->
|
||||
<div class="glass-panel p-8 rounded-xl mb-8 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 p-4 opacity-10">
|
||||
<i class="pi pi-folder text-9xl text-white"></i>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl font-bold text-white mb-4">{{ project.name }}</h1>
|
||||
<p class="text-slate-300 text-lg max-w-2xl mb-6">
|
||||
{{ project.description || 'No description provided.' }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<Button v-if="!isCurrentProject" label="Set as Active" icon="pi pi-check" @click="selectProject" />
|
||||
<Button v-if="isOwner" label="Delete" icon="pi pi-trash" severity="danger" outlined
|
||||
@click="confirmDelete" />
|
||||
<Button label="Settings" icon="pi pi-cog" severity="secondary" outlined />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Members Column -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="glass-panel p-6 rounded-xl h-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-white">Team Members</h2>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<Button icon="pi pi-arrow-left" text rounded @click="router.push('/projects')" />
|
||||
<span v-if="isCurrentProject"
|
||||
class="bg-green-500/20 text-green-400 text-xs px-2 py-1 rounded-full border border-green-500/30 font-medium">
|
||||
Active Project
|
||||
</span>
|
||||
</div>
|
||||
<!-- Inline Add Member -->
|
||||
<div v-if="isOwner" class="mb-6">
|
||||
<div class="flex gap-2">
|
||||
<InputText v-model="inviteUsername" placeholder="Username to add"
|
||||
class="w-full p-inputtext-sm" @keyup.enter="addMember" />
|
||||
<Button label="Add" icon="pi pi-user-plus" size="small" @click="addMember"
|
||||
:loading="inviting" :disabled="!inviteUsername.trim()" />
|
||||
|
||||
<h1 class="text-4xl font-bold text-white mb-4">{{ project.name }}</h1>
|
||||
<p class="text-slate-300 text-lg max-w-2xl mb-6">
|
||||
{{ project.description || 'No description provided.' }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<Button v-if="!isCurrentProject" label="Set as Active" icon="pi pi-check" @click="selectProject" />
|
||||
<Button v-if="isOwner" label="Delete" icon="pi pi-trash" severity="danger" outlined
|
||||
@click="confirmDelete" />
|
||||
<Button label="Settings" icon="pi pi-cog" severity="secondary" outlined />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Members Column -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="glass-panel p-6 rounded-xl h-full">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-white">Team Members</h2>
|
||||
</div>
|
||||
<!-- Inline Add Member -->
|
||||
<div v-if="isOwner" class="mb-6">
|
||||
<div class="flex gap-2">
|
||||
<InputText v-model="inviteUsername" placeholder="Username to add"
|
||||
class="w-full p-inputtext-sm" @keyup.enter="addMember" />
|
||||
<Button label="Add" icon="pi pi-user-plus" size="small" @click="addMember"
|
||||
:loading="inviting" :disabled="!inviteUsername.trim()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-for="memberId in project.members" :key="memberId"
|
||||
class="flex items-center p-3 rounded-lg bg-slate-800/30 border border-slate-700/30">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold mr-3">
|
||||
<i class="pi pi-user"></i>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<p class="text-white font-medium truncate">{{ memberId === project.owner_id ? 'Owner' :
|
||||
'Member' }}</p>
|
||||
<p class="text-slate-500 text-xs truncate">ID: {{ memberId }}</p>
|
||||
</div>
|
||||
<div class="ml-auto" v-if="project.owner_id === memberId">
|
||||
<i class="pi pi-crown text-yellow-500" title="Owner"></i>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-for="member in project.members" :key="member.id"
|
||||
class="flex items-center p-3 rounded-lg bg-slate-800/30 border border-slate-700/30">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold mr-3">
|
||||
<i class="pi pi-user"></i>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<p class="text-white font-bold truncate">{{ member.username }}</p>
|
||||
<p class="text-slate-500 text-[10px] truncate font-mono">ID: {{ member.id }}</p>
|
||||
</div>
|
||||
<div class="ml-auto" v-if="project.owner_id === member.id">
|
||||
<i class="pi pi-crown text-yellow-500" title="Owner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats/Activity Column -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="glass-panel p-6 rounded-xl h-full flex flex-col">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold text-white">Usage Statistics</h2>
|
||||
<Button icon="pi pi-refresh" text rounded @click="fetchUsage" :loading="loadingUsage" />
|
||||
</div>
|
||||
|
||||
<div v-if="usageReport" class="flex-1 flex flex-col gap-8">
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="p-4 rounded-xl bg-slate-800/40 border border-white/5 text-center">
|
||||
<p class="text-slate-500 text-xs uppercase font-bold tracking-wider mb-1">Total Runs</p>
|
||||
<p class="text-2xl font-bold text-white">{{ usageReport.summary?.total_runs || 0 }}</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-xl bg-slate-800/40 border border-white/5 text-center">
|
||||
<p class="text-slate-500 text-xs uppercase font-bold tracking-wider mb-1">Total Tokens</p>
|
||||
<p class="text-2xl font-bold text-white">{{ (usageReport.summary?.total_tokens || 0).toLocaleString() }}</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-xl bg-violet-500/10 border border-violet-500/20 text-center">
|
||||
<p class="text-violet-400 text-xs uppercase font-bold tracking-wider mb-1">Total Spend</p>
|
||||
<p class="text-2xl font-bold text-violet-300">${{ (usageReport.summary?.total_cost || 0).toFixed(2) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Breakdown -->
|
||||
<div v-if="usageReport.by_user && usageReport.by_user.length > 0">
|
||||
<h3 class="text-sm font-bold text-slate-400 uppercase tracking-wider mb-4">Usage by Member</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="item in usageReport.by_user" :key="item.entity_id"
|
||||
class="flex items-center justify-between p-3 rounded-lg bg-slate-800/20 border border-white/5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-slate-700 flex items-center justify-center text-[10px] font-bold">
|
||||
{{ (getMemberUsername(item.entity_id) || '??').substring(0,2).toUpperCase() }}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-slate-300 font-bold truncate max-w-[150px]">{{ getMemberUsername(item.entity_id) || 'Unknown' }}</span>
|
||||
<span class="text-[9px] text-slate-500 font-mono">{{ item.entity_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-6 text-right">
|
||||
<div>
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold">Runs</p>
|
||||
<p class="text-sm text-white">{{ item.stats.total_runs }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] text-slate-500 uppercase font-bold">Cost</p>
|
||||
<p class="text-sm text-violet-300">${{ item.stats.total_cost.toFixed(2) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loadingUsage" class="flex-1 flex items-center justify-center py-20">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-violet-500"></i>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 text-slate-500 flex-1 flex flex-col justify-center">
|
||||
<i class="pi pi-chart-line text-4xl mb-4 opacity-50"></i>
|
||||
<p>No usage data available for this project.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats/Activity Column (Placeholder) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="glass-panel p-6 rounded-xl h-full">
|
||||
<h2 class="text-xl font-bold text-white mb-6">Activity</h2>
|
||||
<div class="text-center py-12 text-slate-500">
|
||||
<i class="pi pi-chart-line text-4xl mb-4 opacity-50"></i>
|
||||
<p>No recent activity registered.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex justify-center items-center h-full">
|
||||
<div class="text-center">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-primary-500 mb-4"></i>
|
||||
<p class="text-slate-400">Loading project...</p>
|
||||
<div v-else class="flex justify-center items-center h-full">
|
||||
<div class="text-center">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-primary-500 mb-4"></i>
|
||||
<p class="text-slate-400">Loading project...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,6 +152,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useProjectsStore } from '@/stores/projectsStore';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { aiService } from '@/services/aiService';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
@@ -113,14 +169,36 @@ const project = computed(() => projectsStore.getProjectById(projectId));
|
||||
const isCurrentProject = computed(() => currentProject.value?.id === projectId);
|
||||
const isOwner = computed(() => authStore.user && project.value && authStore.user.id === project.value.owner_id);
|
||||
|
||||
const getMemberUsername = (memberId) => {
|
||||
if (!project.value || !project.value.members) return null;
|
||||
const member = project.value.members.find(m => m.id === memberId);
|
||||
return member ? member.username : null;
|
||||
};
|
||||
|
||||
const inviteUsername = ref('');
|
||||
const inviting = ref(false);
|
||||
|
||||
const usageReport = ref(null);
|
||||
const loadingUsage = ref(false);
|
||||
|
||||
const fetchUsage = async () => {
|
||||
loadingUsage.value = true;
|
||||
try {
|
||||
// Pass projectId from route params to ensure we get stats for THIS project
|
||||
usageReport.value = await aiService.getUsageReport("user", projectId);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch usage report", e);
|
||||
} finally {
|
||||
loadingUsage.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Ensure projects are loaded
|
||||
if (projects.value.length === 0) {
|
||||
await projectsStore.fetchProjects();
|
||||
}
|
||||
fetchUsage();
|
||||
});
|
||||
|
||||
const selectProject = () => {
|
||||
@@ -164,3 +242,22 @@ const confirmDelete = () => {
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 animate-fade-in">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Projects</h1>
|
||||
<p class="text-slate-400">Manage your workspaces and teams</p>
|
||||
<div class="h-full overflow-y-auto custom-scrollbar">
|
||||
<div class="container mx-auto p-4 md:p-8 animate-fade-in">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Projects</h1>
|
||||
<p class="text-slate-400">Manage your workspaces and teams</p>
|
||||
</div>
|
||||
<Button label="New Project" icon="pi pi-plus" @click="showCreateDialog = true" />
|
||||
</div>
|
||||
<Button label="New Project" icon="pi pi-plus" @click="showCreateDialog = true" />
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Skeleton v-for="i in 3" :key="i" height="150px" class="rounded-xl" />
|
||||
@@ -39,9 +40,15 @@
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between mt-4 border-t border-slate-700/50 pt-4">
|
||||
<div class="flex items-center text-slate-500 text-sm">
|
||||
<i class="pi pi-users mr-2"></i>
|
||||
<span>{{ project.members.length }} members</span>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center text-slate-500 text-sm mb-1">
|
||||
<i class="pi pi-users mr-2"></i>
|
||||
<span>{{ project.members.length }} members</span>
|
||||
</div>
|
||||
<div v-if="projectUsage[project.id]" class="flex items-center text-violet-400 text-xs font-bold">
|
||||
<i class="pi pi-bolt mr-2"></i>
|
||||
<span>${{ projectUsage[project.id].toFixed(2) }} spent</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button v-if="currentProject?.id !== project.id" icon="pi pi-check" label="Select" size="small"
|
||||
@@ -94,8 +101,29 @@ const newProject = ref({
|
||||
description: ''
|
||||
});
|
||||
|
||||
const projectUsage = ref({});
|
||||
|
||||
const fetchUsage = async () => {
|
||||
try {
|
||||
// Fetch usage with project breakdown, pass false to ignore current active project header
|
||||
const report = await aiService.getUsageReport("project", false);
|
||||
if (report && report.by_project) {
|
||||
const usageMap = {};
|
||||
report.by_project.forEach(item => {
|
||||
if (item.entity_id) {
|
||||
usageMap[item.entity_id] = item.stats.total_cost;
|
||||
}
|
||||
});
|
||||
projectUsage.value = usageMap;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch projects usage", e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
projectsStore.fetchProjects();
|
||||
fetchUsage();
|
||||
});
|
||||
|
||||
const createProject = async () => {
|
||||
@@ -121,3 +149,22 @@ const goToProject = (id) => {
|
||||
router.push(`/projects/${id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user