Issue #518 - Subscription not updating after checkout: - Fix stale closure in SubscriptionContext checkout polling (use subscriptionRef) - Move checkout success polling from InitialRouteHandler into SubscriptionContext - Remove redundant polling code from InitialRouteHandler - Fix plan label: 'Free' instead of 'No Plan', proper capitalization - Add plan refresh button in UserBadge - Add 'View Costing Details' to UserBadge dropdown - Rename 'ALwrity Podcast Maker' to 'Podcast Creator' across UI - Clean subscription=success URL param after verification Blog Writer WYSIWYG Editor enhancements: - Per-section preview toggle (view/edit icons) - Enhanced hover-based toolbar - Circular SVG progress stats bar with detailed tooltip - Research tool chips in stats bar footer - Per-section TTS with useTextToSpeech hook (browser native) - Full blog preview modal with print/PDF support - PlayAllTTSButton: sequential playback with progress bar - OnThisPageNav: floating sidebar with scroll tracking - Section data attributes for scroll anchoring GSC Brainstorm Topics feature: - Backend: gsc_brainstorm_service.py (rule-based + LLM recommendations) - Backend: POST /gsc/brainstorm endpoint with 3-word minimum validation - Frontend: gscBrainstorm.ts API client - Frontend: useGSCBrainstormConnection hook (popup OAuth, no /onboarding redirect) - Frontend: useGSCBrainstorm hook (connect check + brainstorm call) - Frontend: GSCBrainstormModal (3-tab results: Opportunities, Gaps, AI Recs) - Frontend: BrainstormButton (visible at 3+ words, GSC connect overlay) - Wire BrainstormButton into ManualResearchForm and ResearchAction - Add blog_writer to gsc_auth router features for ALWRITY_ENABLED_FEATURES
732 lines
26 KiB
TypeScript
732 lines
26 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
|
import {
|
|
Box,
|
|
Paper,
|
|
Typography,
|
|
Grid,
|
|
Stack,
|
|
Button,
|
|
ButtonGroup,
|
|
Tabs,
|
|
Tab,
|
|
Divider,
|
|
CircularProgress,
|
|
Alert,
|
|
Snackbar,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Checkbox,
|
|
} from '@mui/material';
|
|
import {
|
|
Search,
|
|
GridView,
|
|
ViewList,
|
|
Favorite,
|
|
Download,
|
|
Delete,
|
|
Image as ImageIcon,
|
|
Collections,
|
|
History,
|
|
Star,
|
|
Refresh,
|
|
Warning,
|
|
ArrowBack,
|
|
Add,
|
|
InfoOutlined,
|
|
} from '@mui/icons-material';
|
|
import { Tooltip } from '@mui/material';
|
|
import { ImageStudioLayout } from './ImageStudioLayout';
|
|
import { DashboardHeaderProps } from '../shared/types';
|
|
import { useContentAssets, AssetFilters, ContentAsset } from '../../hooks/useContentAssets';
|
|
import { intentResearchApi } from '../../api/intentResearchApi';
|
|
import { AssetFilters as AssetFiltersComponent } from './AssetLibraryComponents/AssetFilters';
|
|
import { AssetCard } from './AssetLibraryComponents/AssetCard';
|
|
import { AssetTableRow } from './AssetLibraryComponents/AssetTableRow';
|
|
|
|
export const AssetLibrary: React.FC = () => {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
|
|
// Initialize filters from URL params if present
|
|
const urlSourceModule = searchParams.get('source_module');
|
|
const urlAssetType = searchParams.get('asset_type');
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [idSearch, setIdSearch] = useState('');
|
|
const [modelSearch, setModelSearch] = useState('');
|
|
const [dateFilter, setDateFilter] = useState('');
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list');
|
|
const [tabValue, setTabValue] = useState(0);
|
|
const [filterType, setFilterType] = useState(() => {
|
|
// Set filter type from URL if present
|
|
if (urlAssetType) {
|
|
return urlAssetType === 'audio' ? 'audio' : urlAssetType === 'image' ? 'images' : urlAssetType === 'video' ? 'videos' : urlAssetType === 'text' ? 'text' : 'all';
|
|
}
|
|
return 'all';
|
|
});
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
const [selectedAssets, setSelectedAssets] = useState<Set<number>>(new Set());
|
|
const [page, setPage] = useState(0);
|
|
const [pageSize] = useState(50);
|
|
const [anchorEl, setAnchorEl] = useState<{ [key: number]: HTMLElement | null }>({});
|
|
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
|
|
open: false,
|
|
message: '',
|
|
severity: 'success',
|
|
});
|
|
const [textPreviews, setTextPreviews] = useState<{ [key: number]: { content: string; loading: boolean; expanded: boolean } }>({});
|
|
|
|
// Debounce search query
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setDebouncedSearch(searchQuery);
|
|
setPage(0);
|
|
}, 300);
|
|
return () => clearTimeout(timer);
|
|
}, [searchQuery]);
|
|
|
|
const onSearch = (value: string) => {
|
|
setSearchQuery(value);
|
|
};
|
|
|
|
// Build filters based on UI state
|
|
const filters: AssetFilters = useMemo(() => {
|
|
const baseFilters: AssetFilters = {
|
|
limit: pageSize,
|
|
offset: page * pageSize,
|
|
};
|
|
|
|
// Apply source_module from URL if present
|
|
if (urlSourceModule) {
|
|
baseFilters.source_module = urlSourceModule as any;
|
|
}
|
|
|
|
// Combine all search terms
|
|
const searchTerms: string[] = [];
|
|
if (debouncedSearch) searchTerms.push(debouncedSearch);
|
|
if (idSearch) searchTerms.push(idSearch);
|
|
if (modelSearch) searchTerms.push(modelSearch);
|
|
|
|
if (searchTerms.length > 0) {
|
|
baseFilters.search = searchTerms.join(' ');
|
|
}
|
|
|
|
if (filterType !== 'all') {
|
|
if (filterType === 'images') baseFilters.asset_type = 'image';
|
|
else if (filterType === 'videos') baseFilters.asset_type = 'video';
|
|
else if (filterType === 'audio') baseFilters.asset_type = 'audio';
|
|
else if (filterType === 'text') baseFilters.asset_type = 'text';
|
|
else if (filterType === 'favorites') baseFilters.favorites_only = true;
|
|
}
|
|
|
|
if (tabValue === 1) {
|
|
baseFilters.favorites_only = true;
|
|
}
|
|
|
|
return baseFilters;
|
|
}, [debouncedSearch, idSearch, modelSearch, filterType, tabValue, page, pageSize, urlSourceModule]);
|
|
|
|
const headerProps: DashboardHeaderProps | undefined = useMemo(() => {
|
|
if (!urlSourceModule) return undefined;
|
|
switch (urlSourceModule) {
|
|
case 'blog_writer':
|
|
return {
|
|
title: 'Blog Posts',
|
|
};
|
|
case 'research_tools':
|
|
return {
|
|
title: 'Research Documents',
|
|
subtitle: 'Access and manage your research projects.',
|
|
};
|
|
case 'product_marketing':
|
|
return {
|
|
title: 'Marketing Assets',
|
|
subtitle: 'Marketing content generated by Product Marketing tools.',
|
|
};
|
|
default:
|
|
return undefined;
|
|
}
|
|
}, [urlSourceModule]);
|
|
|
|
const { assets, loading, error, total, toggleFavorite, deleteAsset, trackUsage, refetch } = useContentAssets(filters);
|
|
|
|
// Refetch assets when component mounts with research_tools filter to show latest drafts
|
|
useEffect(() => {
|
|
if (urlSourceModule === 'research_tools' && urlAssetType === 'text') {
|
|
console.log('[AssetLibrary] Refetching assets for research_tools/text filter');
|
|
const timer = setTimeout(() => {
|
|
refetch();
|
|
}, 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [urlSourceModule, urlAssetType, refetch]);
|
|
|
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
|
setTabValue(newValue);
|
|
setPage(0);
|
|
};
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedAssets(new Set(assets.map(a => a.id)));
|
|
} else {
|
|
setSelectedAssets(new Set());
|
|
}
|
|
};
|
|
|
|
const handleSelectAsset = (assetId: number, checked: boolean) => {
|
|
const newSelected = new Set(selectedAssets);
|
|
if (checked) {
|
|
newSelected.add(assetId);
|
|
} else {
|
|
newSelected.delete(assetId);
|
|
}
|
|
setSelectedAssets(newSelected);
|
|
};
|
|
|
|
const handleBulkDelete = async () => {
|
|
if (selectedAssets.size === 0) return;
|
|
if (!window.confirm(`Delete ${selectedAssets.size} selected asset(s)?`)) return;
|
|
|
|
try {
|
|
await Promise.all(Array.from(selectedAssets).map(id => deleteAsset(id)));
|
|
setSelectedAssets(new Set());
|
|
setSnackbar({ open: true, message: `${selectedAssets.size} asset(s) deleted`, severity: 'success' });
|
|
} catch (err) {
|
|
setSnackbar({ open: true, message: 'Failed to delete assets', severity: 'error' });
|
|
}
|
|
};
|
|
|
|
const handleBulkDownload = async () => {
|
|
if (selectedAssets.size === 0) return;
|
|
|
|
try {
|
|
const selectedAssetsData = assets.filter(a => selectedAssets.has(a.id));
|
|
await Promise.all(selectedAssetsData.map(asset => trackUsage(asset.id, 'download')));
|
|
|
|
// Open each in new tab
|
|
selectedAssetsData.forEach(asset => {
|
|
window.open(asset.file_url, '_blank');
|
|
});
|
|
|
|
setSnackbar({ open: true, message: `Downloading ${selectedAssets.size} asset(s)`, severity: 'success' });
|
|
} catch (err) {
|
|
setSnackbar({ open: true, message: 'Failed to download assets', severity: 'error' });
|
|
}
|
|
};
|
|
|
|
const handleFavorite = async (assetId: number) => {
|
|
try {
|
|
await toggleFavorite(assetId);
|
|
const asset = assets.find(a => a.id === assetId);
|
|
setSnackbar({
|
|
open: true,
|
|
message: asset?.is_favorite ? 'Removed from favorites' : 'Added to favorites',
|
|
severity: 'success',
|
|
});
|
|
} catch (err) {
|
|
setSnackbar({ open: true, message: 'Failed to update favorite', severity: 'error' });
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (assetId: number) => {
|
|
if (!window.confirm('Are you sure you want to delete this asset?')) return;
|
|
try {
|
|
await deleteAsset(assetId);
|
|
setSnackbar({ open: true, message: 'Asset deleted', severity: 'success' });
|
|
} catch (err) {
|
|
setSnackbar({ open: true, message: 'Failed to delete asset', severity: 'error' });
|
|
}
|
|
};
|
|
|
|
const handleDownload = async (asset: ContentAsset) => {
|
|
try {
|
|
await trackUsage(asset.id, 'download');
|
|
window.open(asset.file_url, '_blank');
|
|
} catch (err) {
|
|
console.error('Error downloading:', err);
|
|
}
|
|
};
|
|
|
|
const handleShare = async (asset: ContentAsset) => {
|
|
try {
|
|
await trackUsage(asset.id, 'share');
|
|
if (navigator.share) {
|
|
await navigator.share({
|
|
title: asset.title || asset.filename,
|
|
text: asset.description,
|
|
url: asset.file_url,
|
|
});
|
|
} else {
|
|
await navigator.clipboard.writeText(asset.file_url);
|
|
setSnackbar({ open: true, message: 'Link copied to clipboard', severity: 'success' });
|
|
}
|
|
} catch (err) {
|
|
console.error('Error sharing:', err);
|
|
}
|
|
};
|
|
|
|
const handleMenuOpen = (assetId: number, event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorEl({ ...anchorEl, [assetId]: event.currentTarget });
|
|
};
|
|
|
|
const handleMenuClose = (assetId: number) => {
|
|
setAnchorEl({ ...anchorEl, [assetId]: null });
|
|
};
|
|
|
|
const handleRestoreResearchProject = async (asset: ContentAsset) => {
|
|
try {
|
|
const projectId = asset.asset_metadata?.project_id;
|
|
if (!projectId) {
|
|
setSnackbar({ open: true, message: 'Project ID not found', severity: 'error' });
|
|
return;
|
|
}
|
|
|
|
const project = await intentResearchApi.getResearchProject(projectId);
|
|
|
|
if (!project) {
|
|
setSnackbar({ open: true, message: 'Project not found', severity: 'error' });
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem('alwrity_research_project_id', projectId);
|
|
navigate('/research-dashboard');
|
|
setSnackbar({ open: true, message: 'Research project restored', severity: 'success' });
|
|
} catch (error) {
|
|
console.error('[AssetLibrary] Error restoring research project:', error);
|
|
setSnackbar({ open: true, message: 'Failed to restore research project', severity: 'error' });
|
|
}
|
|
};
|
|
|
|
// Fetch text content for text assets
|
|
const fetchTextContent = async (asset: ContentAsset) => {
|
|
if (asset.asset_type !== 'text' || textPreviews[asset.id]) return;
|
|
|
|
setTextPreviews(prev => ({ ...prev, [asset.id]: { content: '', loading: true, expanded: false } }));
|
|
|
|
try {
|
|
const token = await (window as any).Clerk?.session?.getToken();
|
|
const headers: HeadersInit = {};
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(asset.file_url, { headers });
|
|
if (response.ok) {
|
|
const content = await response.text();
|
|
setTextPreviews(prev => ({ ...prev, [asset.id]: { content, loading: false, expanded: false } }));
|
|
} else {
|
|
throw new Error('Failed to fetch text content');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching text content:', error);
|
|
setTextPreviews(prev => ({
|
|
...prev,
|
|
[asset.id]: { content: 'Failed to load content', loading: false, expanded: false }
|
|
}));
|
|
}
|
|
};
|
|
|
|
const toggleTextPreview = (asset: ContentAsset) => {
|
|
if (asset.asset_type !== 'text') return;
|
|
|
|
if (!textPreviews[asset.id]) {
|
|
fetchTextContent(asset);
|
|
} else {
|
|
setTextPreviews(prev => ({
|
|
...prev,
|
|
[asset.id]: { ...prev[asset.id], expanded: !prev[asset.id].expanded }
|
|
}));
|
|
}
|
|
};
|
|
|
|
const filteredAssets = useMemo(() => {
|
|
let filtered = assets;
|
|
|
|
if (statusFilter !== 'all') {
|
|
filtered = filtered.filter(a => (a.asset_metadata?.status || 'completed') === statusFilter);
|
|
}
|
|
|
|
if (dateFilter) {
|
|
const filterDate = new Date(dateFilter);
|
|
filtered = filtered.filter(a => {
|
|
const assetDate = new Date(a.created_at);
|
|
return assetDate.toDateString() === filterDate.toDateString();
|
|
});
|
|
}
|
|
|
|
return filtered;
|
|
}, [assets, statusFilter, dateFilter]);
|
|
|
|
return (
|
|
<ImageStudioLayout headerProps={headerProps}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
maxWidth: 1600,
|
|
mx: 'auto',
|
|
borderRadius: 4,
|
|
border: '1px solid rgba(255,255,255,0.08)',
|
|
background: 'rgba(15,23,42,0.72)',
|
|
p: { xs: 3, md: 4 },
|
|
backdropFilter: 'blur(25px)',
|
|
}}
|
|
>
|
|
<Stack spacing={3}>
|
|
{/* Header */}
|
|
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
|
|
<Stack direction="row" spacing={2} alignItems="center">
|
|
<Typography
|
|
variant="h3"
|
|
fontWeight={800}
|
|
sx={{
|
|
background: 'linear-gradient(120deg,#ede9fe,#c7d2fe)',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
}}
|
|
>
|
|
{urlSourceModule === 'blog_writer' ? 'Blog Posts' : 'Asset Library'}
|
|
</Typography>
|
|
<Tooltip
|
|
title="Unified content archive for all ALwrity tools: Story Studio, Image Studio, Blog Writer, LinkedIn, Facebook, SEO Tools, and more. Your outputs are stored permanently. Download and organize them for easy access across all your projects."
|
|
arrow
|
|
placement="bottom-start"
|
|
>
|
|
<InfoOutlined sx={{ color: 'rgba(255,255,255,0.4)', fontSize: 20, cursor: 'help' }} />
|
|
</Tooltip>
|
|
</Stack>
|
|
|
|
{/* Context-aware navigation for blog_writer source */}
|
|
{urlSourceModule === 'blog_writer' && (
|
|
<Stack direction="row" spacing={1.5} alignItems="center">
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
startIcon={<ArrowBack />}
|
|
onClick={() => navigate('/blog-writer')}
|
|
sx={{
|
|
color: '#c7d2fe',
|
|
borderColor: 'rgba(99,102,241,0.4)',
|
|
textTransform: 'none',
|
|
'&:hover': {
|
|
borderColor: 'rgba(99,102,241,0.8)',
|
|
background: 'rgba(99,102,241,0.1)',
|
|
},
|
|
}}
|
|
>
|
|
Back to Blog Writer
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
size="small"
|
|
startIcon={<Add />}
|
|
onClick={() => navigate('/blog-writer?new=true')}
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #2563eb 0%, #3b82f6 100%)',
|
|
textTransform: 'none',
|
|
'&:hover': {
|
|
background: 'linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%)',
|
|
},
|
|
}}
|
|
>
|
|
New Blog
|
|
</Button>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
|
|
<Divider sx={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
|
|
|
{/* Advanced Search and Filters */}
|
|
<AssetFiltersComponent
|
|
idSearch={idSearch}
|
|
setIdSearch={setIdSearch}
|
|
modelSearch={modelSearch}
|
|
setModelSearch={setModelSearch}
|
|
dateFilter={dateFilter}
|
|
setDateFilter={setDateFilter}
|
|
statusFilter={statusFilter}
|
|
setStatusFilter={setStatusFilter}
|
|
filterType={filterType}
|
|
setFilterType={setFilterType}
|
|
searchQuery={searchQuery}
|
|
setSearchQuery={setSearchQuery}
|
|
onSearch={onSearch}
|
|
/>
|
|
|
|
{/* Bulk Actions */}
|
|
{selectedAssets.size > 0 && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: 2,
|
|
alignItems: 'center',
|
|
p: 2,
|
|
background: 'rgba(99,102,241,0.1)',
|
|
borderRadius: 2,
|
|
border: '1px solid rgba(99,102,241,0.3)',
|
|
}}
|
|
>
|
|
<Typography variant="body2" sx={{ color: '#c7d2fe' }}>
|
|
{selectedAssets.size} selected
|
|
</Typography>
|
|
<Button
|
|
size="small"
|
|
variant="contained"
|
|
startIcon={<Download />}
|
|
onClick={handleBulkDownload}
|
|
sx={{
|
|
background: 'rgba(59,130,246,0.3)',
|
|
'&:hover': { background: 'rgba(59,130,246,0.5)' },
|
|
}}
|
|
>
|
|
Download
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
variant="contained"
|
|
startIcon={<Delete />}
|
|
onClick={handleBulkDelete}
|
|
sx={{
|
|
background: 'rgba(239,68,68,0.3)',
|
|
'&:hover': { background: 'rgba(239,68,68,0.5)' },
|
|
}}
|
|
>
|
|
Delete
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
onClick={() => setSelectedAssets(new Set())}
|
|
sx={{ ml: 'auto', color: 'rgba(255,255,255,0.6)' }}
|
|
>
|
|
Clear
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<Stack direction="row" spacing={2} alignItems="center">
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<Download />}
|
|
onClick={handleBulkDownload}
|
|
disabled={selectedAssets.size === 0}
|
|
sx={{
|
|
background: 'linear-gradient(90deg,#3b82f6,#2563eb)',
|
|
textTransform: 'none',
|
|
}}
|
|
>
|
|
Download
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<Delete />}
|
|
onClick={handleBulkDelete}
|
|
disabled={selectedAssets.size === 0}
|
|
sx={{
|
|
background: 'linear-gradient(90deg,#ef4444,#dc2626)',
|
|
textTransform: 'none',
|
|
}}
|
|
>
|
|
Delete
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<Refresh />}
|
|
onClick={() => {
|
|
refetch();
|
|
setIdSearch('');
|
|
setModelSearch('');
|
|
setDateFilter('');
|
|
setStatusFilter('all');
|
|
setFilterType('all');
|
|
setSelectedAssets(new Set());
|
|
}}
|
|
sx={{ ml: 'auto', textTransform: 'none' }}
|
|
>
|
|
Reset
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<Search />}
|
|
onClick={() => refetch()}
|
|
sx={{
|
|
background: 'linear-gradient(90deg,#6366f1,#4f46e5)',
|
|
textTransform: 'none',
|
|
}}
|
|
>
|
|
Search
|
|
</Button>
|
|
<ButtonGroup>
|
|
<Button
|
|
variant={viewMode === 'grid' ? 'contained' : 'outlined'}
|
|
onClick={() => setViewMode('grid')}
|
|
startIcon={<GridView />}
|
|
size="small"
|
|
>
|
|
Grid
|
|
</Button>
|
|
<Button
|
|
variant={viewMode === 'list' ? 'contained' : 'outlined'}
|
|
onClick={() => setViewMode('list')}
|
|
startIcon={<ViewList />}
|
|
size="small"
|
|
>
|
|
List
|
|
</Button>
|
|
</ButtonGroup>
|
|
</Stack>
|
|
|
|
{/* Tabs */}
|
|
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.08)' }}>
|
|
<Tabs value={tabValue} onChange={handleTabChange} sx={{ '& .MuiTab-root': { color: 'rgba(255,255,255,0.6)' } }}>
|
|
<Tab icon={<Collections />} iconPosition="start" label="All Assets" />
|
|
<Tab icon={<Favorite />} iconPosition="start" label="Favorites" />
|
|
<Tab icon={<History />} iconPosition="start" label="Recent" />
|
|
<Tab icon={<Star />} iconPosition="start" label="Collections" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
{loading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : error ? (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
) : filteredAssets.length === 0 ? (
|
|
<Box
|
|
sx={{
|
|
textAlign: 'center',
|
|
py: 8,
|
|
color: 'rgba(255,255,255,0.5)',
|
|
}}
|
|
>
|
|
<ImageIcon sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
|
<Typography variant="h6" gutterBottom>
|
|
No assets found
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
{urlSourceModule === 'blog_writer'
|
|
? 'No blog posts found. Generate your first blog post in Blog Writer.'
|
|
: urlSourceModule === 'research_tools'
|
|
? 'No research documents found. Start a new research project.'
|
|
: urlSourceModule === 'product_marketing'
|
|
? 'No marketing assets found. Create one in Product Marketing.'
|
|
: 'Generated content from all ALwrity tools will appear here.'}
|
|
</Typography>
|
|
</Box>
|
|
) : viewMode === 'list' ? (
|
|
<TableContainer>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell padding="checkbox">
|
|
<Checkbox
|
|
checked={selectedAssets.size === filteredAssets.length && filteredAssets.length > 0}
|
|
indeterminate={selectedAssets.size > 0 && selectedAssets.size < filteredAssets.length}
|
|
onChange={e => handleSelectAll(e.target.checked)}
|
|
sx={{ color: 'rgba(255,255,255,0.6)' }}
|
|
/>
|
|
</TableCell>
|
|
<TableCell sx={{ color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>ID</TableCell>
|
|
<TableCell sx={{ color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>Model</TableCell>
|
|
<TableCell sx={{ color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>Status</TableCell>
|
|
<TableCell sx={{ color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>Outputs</TableCell>
|
|
<TableCell sx={{ color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>Created</TableCell>
|
|
<TableCell sx={{ color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>Action</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{filteredAssets.map(asset => (
|
|
<AssetTableRow
|
|
key={asset.id}
|
|
asset={asset}
|
|
isSelected={selectedAssets.has(asset.id)}
|
|
onSelect={(checked) => handleSelectAsset(asset.id, checked)}
|
|
onDownload={handleDownload}
|
|
onShare={handleShare}
|
|
onDelete={handleDelete}
|
|
onFavorite={handleFavorite}
|
|
onRestore={handleRestoreResearchProject}
|
|
onMenuOpen={handleMenuOpen}
|
|
onMenuClose={handleMenuClose}
|
|
anchorEl={anchorEl[asset.id]}
|
|
textPreview={textPreviews[asset.id]}
|
|
onToggleTextPreview={toggleTextPreview}
|
|
onCopyId={(id) => {
|
|
navigator.clipboard.writeText(id);
|
|
setSnackbar({ open: true, message: 'ID copied', severity: 'success' });
|
|
}}
|
|
/>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
) : (
|
|
<Grid container spacing={3}>
|
|
{filteredAssets.map(asset => (
|
|
<Grid item xs={12} sm={6} md={4} lg={3} key={asset.id}>
|
|
<AssetCard
|
|
asset={asset}
|
|
textPreview={textPreviews[asset.id]}
|
|
onToggleTextPreview={toggleTextPreview}
|
|
onFavorite={handleFavorite}
|
|
onDownload={handleDownload}
|
|
onShare={handleShare}
|
|
onDelete={handleDelete}
|
|
onRestore={handleRestoreResearchProject}
|
|
/>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{total > pageSize && (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4, gap: 2, alignItems: 'center' }}>
|
|
<Button
|
|
disabled={page === 0}
|
|
onClick={() => setPage(p => Math.max(0, p - 1))}
|
|
sx={{ color: 'rgba(255,255,255,0.8)' }}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
|
Page {page + 1} of {Math.ceil(total / pageSize)} ({total} total)
|
|
</Typography>
|
|
<Button
|
|
disabled={(page + 1) * pageSize >= total}
|
|
onClick={() => setPage(p => p + 1)}
|
|
sx={{ color: 'rgba(255,255,255,0.8)' }}
|
|
>
|
|
Next
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
<Snackbar
|
|
open={snackbar.open}
|
|
autoHideDuration={3000}
|
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
>
|
|
<Alert severity={snackbar.severity} onClose={() => setSnackbar({ ...snackbar, open: false })}>
|
|
{snackbar.message}
|
|
</Alert>
|
|
</Snackbar>
|
|
</Stack>
|
|
</Paper>
|
|
</ImageStudioLayout>
|
|
);
|
|
};
|