fix fix fi x fix

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 15:30:15 +00:00
parent 7d4d309677
commit e8360a6703
11 changed files with 488 additions and 71 deletions

View File

@@ -90,13 +90,16 @@ export default function SiteCard({
disabled={isToggling}
onChange={handleToggle}
/>
<Badge
variant="solid"
color={site.is_active ? "success" : "error"}
size="xs"
>
{site.is_active ? 'Active' : 'Inactive'}
</Badge>
<div className="flex flex-col items-center gap-1">
<Badge
variant="solid"
color={site.is_active ? "success" : "error"}
size="xs"
>
{site.is_active ? 'Active' : 'Inactive'}
</Badge>
<span className="text-xs text-gray-500">Button</span>
</div>
</div>
</div>
<div className="flex items-center justify-center border-t border-gray-200 p-2 dark:border-gray-800">
@@ -128,16 +131,15 @@ export default function SiteCard({
>
Settings
</Button>
{onDelete && (
<Button
variant="outline"
tone="destructive"
size="sm"
onClick={() => onDelete(site)}
startIcon={<TrashBinIcon className="w-4 h-4" />}
title="Delete site"
/>
)}
<Button
variant="primary"
tone="danger"
size="sm"
onClick={() => onDelete && onDelete(site)}
startIcon={<TrashBinIcon className="w-4 h-4" />}
>
Delete
</Button>
</div>
</div>
</article>

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import PageMeta from '../../components/common/PageMeta';
import SiteCard from '../../components/common/SiteCard';
import FormModal, { FormField } from '../../components/common/FormModal';
import ConfirmDialog from '../../components/common/ConfirmDialog';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Alert from '../../components/ui/alert/Alert';
@@ -39,6 +40,9 @@ export default function Sites() {
const [showSiteModal, setShowSiteModal] = useState(false);
const [showSectorsModal, setShowSectorsModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [siteToDelete, setSiteToDelete] = useState<Site | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
const [industries, setIndustries] = useState<Industry[]>([]);
@@ -293,13 +297,17 @@ export default function Sites() {
};
const handleDeleteSite = async (site: Site) => {
if (!window.confirm(`Are you sure you want to delete "${site.name}"? This action cannot be undone.`)) {
return;
}
const handleDeleteSite = (site: Site) => {
setSiteToDelete(site);
setShowDeleteConfirm(true);
};
const confirmDeleteSite = async () => {
if (!siteToDelete) return;
setIsDeleting(true);
try {
await deleteSite(site.id);
await deleteSite(siteToDelete.id);
toast.success('Site deleted successfully');
await loadSites();
if (showDetailsModal) {
@@ -307,6 +315,10 @@ export default function Sites() {
}
} catch (error: any) {
toast.error(`Failed to delete site: ${error.message}`);
} finally {
setIsDeleting(false);
setShowDeleteConfirm(false);
setSiteToDelete(null);
}
};
@@ -604,6 +616,22 @@ export default function Sites() {
/>
)}
</div>
{/* Delete Confirmation Dialog */}
<ConfirmDialog
isOpen={showDeleteConfirm}
onClose={() => {
setShowDeleteConfirm(false);
setSiteToDelete(null);
}}
onConfirm={confirmDeleteSite}
title="Delete Site"
message={`Are you sure you want to delete "${siteToDelete?.name}"? This action cannot be undone.`}
confirmText="Delete"
cancelText="Cancel"
variant="danger"
isLoading={isDeleting}
/>
</>
);
}

View File

@@ -49,6 +49,17 @@ interface ContentGenerationSettings {
defaultLength: string;
}
// AI Model Config from API
interface AIModelConfig {
model_name: string;
display_name: string;
model_type: string;
provider: string;
valid_sizes?: string[];
quality_tier?: string;
credits_per_image?: number;
}
// Map user-friendly quality to internal service/model configuration
const QUALITY_TO_CONFIG: Record<string, { service: 'openai' | 'runware'; model: string }> = {
standard: { service: 'openai', model: 'dall-e-2' },
@@ -63,8 +74,24 @@ const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'p
return 'standard';
};
// Get available image sizes based on provider and model
const getImageSizes = (provider: string, model: string) => {
// Get available image sizes based on provider and model (from API or fallback)
const getImageSizes = (provider: string, model: string, imageModels: AIModelConfig[]) => {
// First, try to find the model in the fetched models
const modelConfig = imageModels.find(m =>
m.model_name === model ||
(m.provider === provider && m.model_name.includes(model))
);
// If found and has valid_sizes, use them
if (modelConfig?.valid_sizes && modelConfig.valid_sizes.length > 0) {
return modelConfig.valid_sizes.map(size => ({
value: size,
label: `${size.replace('x', '×')} pixels`
}));
}
// Fallback to hardcoded sizes for backward compatibility
if (provider === 'runware') {
return [
{ value: '1280x832', label: '1280×832 pixels' },
@@ -101,6 +128,9 @@ export default function ContentSettingsPage() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Image Models from API - for dynamic size options
const [imageModels, setImageModels] = useState<AIModelConfig[]>([]);
// Content Generation Settings
const [contentSettings, setContentSettings] = useState<ContentGenerationSettings>({
appendToPrompt: '',
@@ -141,20 +171,21 @@ export default function ContentSettingsPage() {
};
}, [imageQuality]);
// Get available sizes for current quality
// Get available sizes for current quality (uses imageModels from API)
const availableSizes = getImageSizes(
getCurrentConfig().service,
getCurrentConfig().model
getCurrentConfig().model,
imageModels
);
useEffect(() => {
loadSettings();
}, []);
// Update image sizes when quality changes
// Update image sizes when quality changes or imageModels are loaded
useEffect(() => {
const config = getCurrentConfig();
const sizes = getImageSizes(config.service, config.model);
const sizes = getImageSizes(config.service, config.model, imageModels);
const defaultSize = sizes.length > 0 ? sizes[0].value : '1024x1024';
const validSizes = sizes.map(s => s.value);
@@ -178,12 +209,24 @@ export default function ContentSettingsPage() {
model: config.model,
}));
}
}, [imageQuality, getCurrentConfig]);
}, [imageQuality, getCurrentConfig, imageModels]);
const loadSettings = async () => {
try {
setLoading(true);
// Load available image models from API (for dynamic sizes)
try {
const modelsResponse = await fetchAPI('/v1/billing/models/?type=image');
if (modelsResponse?.data) {
setImageModels(modelsResponse.data);
} else if (Array.isArray(modelsResponse)) {
setImageModels(modelsResponse);
}
} catch (err) {
console.log('Image models not available, using hardcoded sizes');
}
// Load image generation settings
const imageData = await fetchAPI('/v1/system/settings/integrations/image_generation/');
if (imageData) {

View File

@@ -16,10 +16,11 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Content, fetchImages, ImageRecord } from '../services/api';
import { Content, fetchImages, ImageRecord, fetchAPI } from '../services/api';
import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon, PencilIcon, ImageIcon, BoltIcon } from '../icons';
import { useNavigate } from 'react-router-dom';
import Button from '../components/ui/button/Button';
import { useToast } from '../components/ui/toast/ToastContainer';
interface ContentViewTemplateProps {
content: Content | null;
@@ -618,9 +619,47 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm
export default function ContentViewTemplate({ content, loading, onBack }: ContentViewTemplateProps) {
const navigate = useNavigate();
const toast = useToast();
const [imageRecords, setImageRecords] = useState<ImageRecord[]>([]);
const [imagesLoading, setImagesLoading] = useState(false);
const [imagesError, setImagesError] = useState<string | null>(null);
// Schedule editing state
const [isEditingSchedule, setIsEditingSchedule] = useState(false);
const [scheduleDateTime, setScheduleDateTime] = useState<string>('');
const [isUpdatingSchedule, setIsUpdatingSchedule] = useState(false);
// Initialize schedule datetime when content loads
useEffect(() => {
if (content?.scheduled_publish_at) {
// Convert ISO string to datetime-local format (YYYY-MM-DDTHH:mm)
const date = new Date(content.scheduled_publish_at);
const localDateTime = date.toISOString().slice(0, 16);
setScheduleDateTime(localDateTime);
}
}, [content?.scheduled_publish_at]);
// Handler to update schedule
const handleUpdateSchedule = async () => {
if (!content?.id || !scheduleDateTime) return;
setIsUpdatingSchedule(true);
try {
const isoDateTime = new Date(scheduleDateTime).toISOString();
await fetchAPI(`/v1/writer/content/${content.id}/schedule/`, {
method: 'POST',
body: JSON.stringify({ scheduled_publish_at: isoDateTime }),
});
toast.success('Schedule updated successfully');
setIsEditingSchedule(false);
// Trigger content refresh by reloading the page
window.location.reload();
} catch (error: any) {
toast.error(`Failed to update schedule: ${error.message}`);
} finally {
setIsUpdatingSchedule(false);
}
};
const metadataPrompts = useMemo(() => extractImagePromptsFromMetadata(content?.metadata), [content?.metadata]);
@@ -1140,11 +1179,60 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
{content.site_status === 'failed' && 'Failed'}
</span>
</div>
{content.scheduled_publish_at && content.site_status === 'scheduled' && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formatDate(content.scheduled_publish_at)}
</span>
{/* Schedule Date/Time Editor - Only for scheduled content */}
{content.site_status === 'scheduled' && (
<div className="flex items-center gap-2">
{isEditingSchedule ? (
<>
<input
type="datetime-local"
value={scheduleDateTime}
onChange={(e) => setScheduleDateTime(e.target.value)}
className="text-sm px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
<Button
size="xs"
variant="primary"
tone="brand"
onClick={handleUpdateSchedule}
disabled={isUpdatingSchedule || !scheduleDateTime}
>
{isUpdatingSchedule ? 'Updating...' : 'Update'}
</Button>
<Button
size="xs"
variant="ghost"
tone="neutral"
onClick={() => {
setIsEditingSchedule(false);
// Reset to original value
if (content.scheduled_publish_at) {
const date = new Date(content.scheduled_publish_at);
setScheduleDateTime(date.toISOString().slice(0, 16));
}
}}
>
Cancel
</Button>
</>
) : (
<>
<span className="text-sm text-gray-500 dark:text-gray-400">
{content.scheduled_publish_at ? formatDate(content.scheduled_publish_at) : ''}
</span>
<button
onClick={() => setIsEditingSchedule(true)}
className="text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 p-1"
title="Edit schedule"
>
<PencilIcon className="w-4 h-4" />
</button>
</>
)}
</div>
)}
{content.external_url && content.site_status === 'published' && (
<a
href={content.external_url}