188 lines
7.4 KiB
TypeScript
188 lines
7.4 KiB
TypeScript
import { ReactNode, useState, useEffect } from 'react';
|
|
import Switch from '../form/switch/Switch';
|
|
import Button from '../ui/button/Button';
|
|
import { usePersistentToggle } from '../../hooks/usePersistentToggle';
|
|
import { useToast } from '../ui/toast/ToastContainer';
|
|
|
|
type ValidationStatus = 'not_configured' | 'pending' | 'success' | 'error';
|
|
|
|
interface ImageServiceCardProps {
|
|
icon: ReactNode;
|
|
title: string;
|
|
description: string;
|
|
validationStatus: ValidationStatus;
|
|
onSettings: () => void;
|
|
onDetails: () => void;
|
|
}
|
|
|
|
/**
|
|
* Image Generation Service Card Component
|
|
* Manages default image generation service and model selection app-wide
|
|
* This is separate from individual API integrations (OpenAI/Runware)
|
|
*/
|
|
export default function ImageServiceCard({
|
|
icon,
|
|
title,
|
|
description,
|
|
validationStatus,
|
|
onSettings,
|
|
onDetails,
|
|
}: ImageServiceCardProps) {
|
|
const toast = useToast();
|
|
|
|
// Use built-in persistent toggle for image generation service
|
|
const persistentToggle = usePersistentToggle({
|
|
resourceId: 'image_generation',
|
|
getEndpoint: '/v1/system/settings/integrations/{id}/',
|
|
saveEndpoint: '/v1/system/settings/integrations/{id}/save/',
|
|
initialEnabled: false,
|
|
onToggleSuccess: (enabled) => {
|
|
toast.success(`Image generation service ${enabled ? 'enabled' : 'disabled'}`);
|
|
},
|
|
onToggleError: (error) => {
|
|
toast.error(`Failed to update image generation service: ${error.message}`);
|
|
},
|
|
});
|
|
|
|
const enabled = persistentToggle.enabled;
|
|
const isToggling = persistentToggle.loading;
|
|
const [imageSettings, setImageSettings] = useState<{ service?: string; model?: string; runwareModel?: string }>({});
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
|
|
|
// Load image settings to get provider and model
|
|
useEffect(() => {
|
|
const loadSettings = async () => {
|
|
try {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/v1/system/settings/integrations/image_generation/`,
|
|
{ credentials: 'include' }
|
|
);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.success && data.data) {
|
|
setImageSettings(data.data);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading image settings:', error);
|
|
}
|
|
};
|
|
loadSettings();
|
|
}, [API_BASE_URL, enabled]); // Reload when enabled changes
|
|
|
|
const handleToggle = (newEnabled: boolean) => {
|
|
persistentToggle.toggle(newEnabled);
|
|
};
|
|
|
|
// Get provider and model display text
|
|
const getProviderModelText = () => {
|
|
const service = imageSettings.service || 'openai';
|
|
if (service === 'openai') {
|
|
const model = imageSettings.model || 'dall-e-3';
|
|
const modelNames: Record<string, string> = {
|
|
'dall-e-3': 'DALL·E 3',
|
|
'dall-e-2': 'DALL·E 2',
|
|
'gpt-image-1': 'GPT Image 1 (Full)',
|
|
'gpt-image-1-mini': 'GPT Image 1 Mini',
|
|
};
|
|
return `OpenAI ${modelNames[model] || model}`;
|
|
} else if (service === 'runware') {
|
|
const model = imageSettings.runwareModel || 'runware:97@1';
|
|
// Map model ID to display name
|
|
const modelDisplayNames: Record<string, string> = {
|
|
'runware:97@1': 'HiDream-I1 Full',
|
|
'runware:gen3a_turbo': 'Gen3a Turbo',
|
|
'runware:gen3a': 'Gen3a',
|
|
};
|
|
const displayName = modelDisplayNames[model] || model;
|
|
return `Runware ${displayName}`;
|
|
}
|
|
return 'Not configured';
|
|
};
|
|
|
|
// Get text color based on provider and status
|
|
const getTextColor = () => {
|
|
const service = imageSettings.service || 'openai';
|
|
const isConfigured = service && (imageSettings.model || imageSettings.runwareModel);
|
|
|
|
// Grey if not configured or pending
|
|
if (!isConfigured || validationStatus === 'not_configured' || validationStatus === 'pending') {
|
|
return 'text-gray-400 dark:text-gray-500';
|
|
}
|
|
|
|
// Black for both OpenAI and Runware when configured
|
|
return 'text-black dark:text-white';
|
|
};
|
|
|
|
return (
|
|
<article className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3">
|
|
<div className="relative p-5 pb-9">
|
|
<div className="mb-5 inline-flex h-10 w-10 items-center justify-center">
|
|
{icon}
|
|
</div>
|
|
<h3 className="mb-3 text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
{title}
|
|
</h3>
|
|
<p className="max-w-xs text-sm text-gray-500 dark:text-gray-400">
|
|
{description}
|
|
</p>
|
|
{/* Provider + Model Text - Same size as heading */}
|
|
<div className="absolute top-5 right-5 h-fit">
|
|
<p className={`text-lg font-semibold ${getTextColor()} transition-colors duration-200`}>
|
|
{getProviderModelText()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between border-t border-gray-200 p-5 dark:border-gray-800">
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="outline"
|
|
size="md"
|
|
onClick={onSettings}
|
|
className="shadow-theme-xs inline-flex h-11 w-11 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 20 20"
|
|
fill="none"
|
|
>
|
|
<path
|
|
d="M5.64615 4.59906C5.05459 4.25752 4.29808 4.46015 3.95654 5.05171L2.69321 7.23986C2.35175 7.83128 2.5544 8.58754 3.14582 8.92899C3.97016 9.40493 3.97017 10.5948 3.14583 11.0707C2.55441 11.4122 2.35178 12.1684 2.69323 12.7598L3.95657 14.948C4.2981 15.5395 5.05461 15.7422 5.64617 15.4006C6.4706 14.9247 7.50129 15.5196 7.50129 16.4715C7.50129 17.1545 8.05496 17.7082 8.73794 17.7082H11.2649C11.9478 17.7082 12.5013 17.1545 12.5013 16.4717C12.5013 15.5201 13.5315 14.9251 14.3556 15.401C14.9469 15.7423 15.7029 15.5397 16.0443 14.9485L17.3079 12.7598C17.6494 12.1684 17.4467 11.4121 16.8553 11.0707C16.031 10.5948 16.031 9.40494 16.8554 8.92902C17.4468 8.58757 17.6494 7.83133 17.3079 7.23992L16.0443 5.05123C15.7029 4.45996 14.9469 4.25737 14.3556 4.59874C13.5315 5.07456 12.5013 4.47961 12.5013 3.52798C12.5013 2.84515 11.9477 2.2915 11.2649 2.2915L8.73795 2.2915C8.05496 2.2915 7.50129 2.84518 7.50129 3.52816C7.50129 4.48015 6.47059 5.07505 5.64615 4.59906Z"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
<path
|
|
d="M12.5714 9.99977C12.5714 11.4196 11.4204 12.5706 10.0005 12.5706C8.58069 12.5706 7.42969 11.4196 7.42969 9.99977C7.42969 8.57994 8.58069 7.42894 10.0005 7.42894C11.4204 7.42894 12.5714 8.57994 12.5714 9.99977Z"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="md"
|
|
onClick={onDetails}
|
|
className="shadow-theme-xs inline-flex h-11 items-center justify-center rounded-lg border border-gray-300 px-4 py-3 text-sm font-medium text-gray-700 dark:border-gray-700 dark:text-gray-400"
|
|
>
|
|
Details
|
|
</Button>
|
|
</div>
|
|
<Switch
|
|
label=""
|
|
checked={enabled}
|
|
disabled={isToggling}
|
|
onChange={handleToggle}
|
|
/>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|