Merge branch 'main' of https://git.igny8.com/salman/igny8
This commit is contained in:
@@ -4,6 +4,8 @@ import { HelmetProvider } from "react-helmet-async";
|
||||
import AppLayout from "./layout/AppLayout";
|
||||
import { ScrollToTop } from "./components/common/ScrollToTop";
|
||||
import ProtectedRoute from "./components/auth/ProtectedRoute";
|
||||
import ModuleGuard from "./components/common/ModuleGuard";
|
||||
import { AwsAdminGuard } from "./components/auth/AwsAdminGuard";
|
||||
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
||||
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
||||
import { useAuthStore } from "./store/authStore";
|
||||
@@ -67,6 +69,9 @@ const AccountSettingsPage = lazy(() => import("./pages/account/AccountSettingsPa
|
||||
const TeamManagementPage = lazy(() => import("./pages/account/TeamManagementPage"));
|
||||
const UsageAnalyticsPage = lazy(() => import("./pages/account/UsageAnalyticsPage"));
|
||||
|
||||
// Admin Module - Only dashboard for aws-admin users
|
||||
const AdminSystemDashboard = lazy(() => import("./pages/admin/AdminSystemDashboard"));
|
||||
|
||||
// Reference Data - Lazy loaded
|
||||
const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
|
||||
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
|
||||
@@ -81,6 +86,7 @@ const Users = lazy(() => import("./pages/Settings/Users"));
|
||||
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
|
||||
const SystemSettings = lazy(() => import("./pages/Settings/System"));
|
||||
const AccountSettings = lazy(() => import("./pages/Settings/Account"));
|
||||
const ModuleSettings = lazy(() => import("./pages/Settings/Modules"));
|
||||
const AISettings = lazy(() => import("./pages/Settings/AI"));
|
||||
const Plans = lazy(() => import("./pages/Settings/Plans"));
|
||||
const Industries = lazy(() => import("./pages/Settings/Industries"));
|
||||
@@ -147,42 +153,115 @@ export default function App() {
|
||||
|
||||
{/* Planner Module - Redirect dashboard to keywords */}
|
||||
<Route path="/planner" element={<Navigate to="/planner/keywords" replace />} />
|
||||
<Route path="/planner/keywords" element={<Keywords />} />
|
||||
<Route path="/planner/clusters" element={<Clusters />} />
|
||||
<Route path="/planner/clusters/:id" element={<ClusterDetail />} />
|
||||
<Route path="/planner/ideas" element={<Ideas />} />
|
||||
<Route path="/planner/keywords" element={
|
||||
<ModuleGuard module="planner">
|
||||
<Keywords />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/clusters" element={
|
||||
<ModuleGuard module="planner">
|
||||
<Clusters />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/clusters/:id" element={
|
||||
<ModuleGuard module="planner">
|
||||
<ClusterDetail />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/planner/ideas" element={
|
||||
<ModuleGuard module="planner">
|
||||
<Ideas />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
|
||||
{/* Writer Module - Redirect dashboard to tasks */}
|
||||
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
|
||||
<Route path="/writer/tasks" element={<Tasks />} />
|
||||
<Route path="/writer/tasks" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Tasks />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
{/* Writer Content Routes - Order matters: list route must come before detail route */}
|
||||
<Route path="/writer/content" element={<Content />} />
|
||||
<Route path="/writer/content" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Content />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
|
||||
<Route path="/writer/content/:id" element={<ContentView />} />
|
||||
<Route path="/writer/content/:id" element={
|
||||
<ModuleGuard module="writer">
|
||||
<ContentView />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
|
||||
<Route path="/writer/images" element={<Images />} />
|
||||
<Route path="/writer/review" element={<Review />} />
|
||||
<Route path="/writer/published" element={<Published />} />
|
||||
<Route path="/writer/images" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Images />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/review" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Review />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/writer/published" element={
|
||||
<ModuleGuard module="writer">
|
||||
<Published />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
|
||||
{/* Automation Module */}
|
||||
<Route path="/automation" element={<AutomationPage />} />
|
||||
|
||||
{/* Linker Module - Redirect dashboard to content */}
|
||||
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />
|
||||
<Route path="/linker/content" element={<LinkerContentList />} />
|
||||
<Route path="/linker/content" element={
|
||||
<ModuleGuard module="linker">
|
||||
<LinkerContentList />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
|
||||
{/* Optimizer Module - Redirect dashboard to content */}
|
||||
<Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} />
|
||||
<Route path="/optimizer/content" element={<OptimizerContentSelector />} />
|
||||
<Route path="/optimizer/analyze/:id" element={<AnalysisPreview />} />
|
||||
<Route path="/optimizer/content" element={
|
||||
<ModuleGuard module="optimizer">
|
||||
<OptimizerContentSelector />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/optimizer/analyze/:id" element={
|
||||
<ModuleGuard module="optimizer">
|
||||
<AnalysisPreview />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
|
||||
{/* Thinker Module */}
|
||||
{/* Thinker Module - Redirect dashboard to prompts */}
|
||||
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
|
||||
<Route path="/thinker/prompts" element={<Prompts />} />
|
||||
<Route path="/thinker/author-profiles" element={<AuthorProfiles />} />
|
||||
<Route path="/thinker/profile" element={<ThinkerProfile />} />
|
||||
<Route path="/thinker/strategies" element={<Strategies />} />
|
||||
<Route path="/thinker/image-testing" element={<ImageTesting />} />
|
||||
<Route path="/thinker/prompts" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<Prompts />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/author-profiles" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<AuthorProfiles />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/profile" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<ThinkerProfile />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/strategies" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<Strategies />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
<Route path="/thinker/image-testing" element={
|
||||
<ModuleGuard module="thinker">
|
||||
<ImageTesting />
|
||||
</ModuleGuard>
|
||||
} />
|
||||
|
||||
{/* Billing Module */}
|
||||
<Route path="/billing" element={<Navigate to="/billing/overview" replace />} />
|
||||
@@ -198,6 +277,13 @@ export default function App() {
|
||||
<Route path="/account/team" element={<TeamManagementPage />} />
|
||||
<Route path="/account/usage" element={<UsageAnalyticsPage />} />
|
||||
|
||||
{/* Admin Routes - Only Dashboard for aws-admin users */}
|
||||
<Route path="/admin/dashboard" element={
|
||||
<AwsAdminGuard>
|
||||
<AdminSystemDashboard />
|
||||
</AwsAdminGuard>
|
||||
} />
|
||||
|
||||
{/* Reference Data */}
|
||||
<Route path="/reference/seed-keywords" element={<SeedKeywords />} />
|
||||
<Route path="/planner/keyword-opportunities" element={<KeywordOpportunities />} />
|
||||
@@ -215,6 +301,7 @@ export default function App() {
|
||||
<Route path="/settings/subscriptions" element={<Subscriptions />} />
|
||||
<Route path="/settings/system" element={<SystemSettings />} />
|
||||
<Route path="/settings/account" element={<AccountSettings />} />
|
||||
<Route path="/settings/modules" element={<ModuleSettings />} />
|
||||
<Route path="/settings/ai" element={<AISettings />} />
|
||||
<Route path="/settings/plans" element={<Plans />} />
|
||||
<Route path="/settings/industries" element={<Industries />} />
|
||||
|
||||
31
frontend/src/components/auth/AwsAdminGuard.tsx
Normal file
31
frontend/src/components/auth/AwsAdminGuard.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
interface AwsAdminGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard that only allows access to users of the aws-admin account
|
||||
* Used for the single remaining admin dashboard page
|
||||
*/
|
||||
export const AwsAdminGuard: React.FC<AwsAdminGuardProps> = ({ children }) => {
|
||||
const { user, loading } = useAuthStore();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user belongs to aws-admin account
|
||||
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||
|
||||
if (!isAwsAdmin) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -13,12 +12,12 @@ interface ImageServiceCardProps {
|
||||
validationStatus: ValidationStatus;
|
||||
onSettings: () => void;
|
||||
onDetails: () => void;
|
||||
onToggleSuccess?: (enabled: boolean, data?: any) => void; // Callback when toggle succeeds
|
||||
}
|
||||
|
||||
/**
|
||||
* Image Generation Service Card Component
|
||||
* Manages default image generation service and model selection app-wide
|
||||
* This is separate from individual API integrations (OpenAI/Runware)
|
||||
* Manages default image generation service enable/disable state
|
||||
*/
|
||||
export default function ImageServiceCard({
|
||||
icon,
|
||||
@@ -27,32 +26,20 @@ export default function ImageServiceCard({
|
||||
validationStatus,
|
||||
onSettings,
|
||||
onDetails,
|
||||
onToggleSuccess,
|
||||
}: 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 [enabled, setEnabled] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [imageSettings, setImageSettings] = useState<{ service?: string; provider?: string; model?: string; imageModel?: 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
|
||||
// Load image settings
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/v1/system/settings/integrations/image_generation/`,
|
||||
@@ -62,38 +49,67 @@ export default function ImageServiceCard({
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
setImageSettings(data.data);
|
||||
setEnabled(data.data.enabled || false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading image settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadSettings();
|
||||
}, [API_BASE_URL, enabled]); // Reload when enabled changes
|
||||
}, [API_BASE_URL]);
|
||||
|
||||
const handleToggle = (newEnabled: boolean) => {
|
||||
persistentToggle.toggle(newEnabled);
|
||||
// Handle toggle
|
||||
const handleToggle = async (newEnabled: boolean) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/v1/system/settings/integrations/image_generation/save/`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ ...imageSettings, enabled: newEnabled }),
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
setEnabled(newEnabled);
|
||||
toast.success(`Image generation service ${newEnabled ? 'enabled' : 'disabled'}`);
|
||||
|
||||
// Call onToggleSuccess callback with enabled state and settings data
|
||||
if (onToggleSuccess) {
|
||||
onToggleSuccess(newEnabled, imageSettings);
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to update image generation service');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling image generation:', error);
|
||||
toast.error('Failed to update image generation service');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get provider and model display text
|
||||
const getProviderModelText = () => {
|
||||
const service = imageSettings.service || 'openai';
|
||||
const service = imageSettings.service || imageSettings.provider || 'openai';
|
||||
if (service === 'openai') {
|
||||
const model = imageSettings.model || 'dall-e-3';
|
||||
const model = imageSettings.model || imageSettings.imageModel || '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';
|
||||
const model = imageSettings.runwareModel || imageSettings.model || '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',
|
||||
'runware:100@1': 'Runware 100@1',
|
||||
'runware:101@1': 'Runware 101@1',
|
||||
};
|
||||
const displayName = modelDisplayNames[model] || model;
|
||||
return `Runware ${displayName}`;
|
||||
@@ -177,7 +193,7 @@ export default function ImageServiceCard({
|
||||
<Switch
|
||||
label=""
|
||||
checked={enabled}
|
||||
disabled={isToggling}
|
||||
disabled={loading || isSaving}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -47,11 +47,7 @@ const GSCIcon = () => (
|
||||
interface IntegrationConfig {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
apiKey?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
authBaseUri?: string;
|
||||
appName?: string;
|
||||
// Note: API keys are configured platform-wide in GlobalIntegrationSettings (not user-editable)
|
||||
model?: string;
|
||||
// Image generation service settings (separate from API integrations)
|
||||
service?: string; // 'openai' or 'runware'
|
||||
@@ -74,13 +70,12 @@ export default function Integration() {
|
||||
openai: {
|
||||
id: 'openai',
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
model: 'gpt-4.1',
|
||||
model: 'gpt-4o-mini',
|
||||
},
|
||||
runware: {
|
||||
id: 'runware',
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
model: 'runware:97@1',
|
||||
},
|
||||
image_generation: {
|
||||
id: 'image_generation',
|
||||
@@ -105,6 +100,17 @@ export default function Integration() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
|
||||
// Available models from AIModelConfig
|
||||
const [availableModels, setAvailableModels] = useState<{
|
||||
openai_text: Array<{ value: string; label: string }>;
|
||||
openai_image: Array<{ value: string; label: string }>;
|
||||
runware_image: Array<{ value: string; label: string }>;
|
||||
}>({
|
||||
openai_text: [],
|
||||
openai_image: [],
|
||||
runware_image: [],
|
||||
});
|
||||
|
||||
// Validation status for each integration: 'not_configured' | 'pending' | 'success' | 'error'
|
||||
const [validationStatuses, setValidationStatuses] = useState<Record<string, 'not_configured' | 'pending' | 'success' | 'error'>>({
|
||||
openai: 'not_configured',
|
||||
@@ -119,18 +125,27 @@ export default function Integration() {
|
||||
const validateIntegration = useCallback(async (
|
||||
integrationId: string,
|
||||
enabled: boolean,
|
||||
apiKey?: string,
|
||||
model?: string
|
||||
) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
||||
|
||||
// Only validate OpenAI and Runware (GSC doesn't have validation endpoint)
|
||||
// Image generation doesn't have a test endpoint - just set status based on enabled
|
||||
if (integrationId === 'image_generation') {
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: enabled ? 'success' : 'not_configured',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate OpenAI and Runware (they have test endpoints)
|
||||
if (!['openai', 'runware'].includes(integrationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if integration is enabled
|
||||
// If disabled, mark as not_configured (not error!)
|
||||
if (!enabled) {
|
||||
// Not configured or disabled - set status accordingly
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'not_configured',
|
||||
@@ -138,38 +153,29 @@ export default function Integration() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set pending status
|
||||
// Integration is enabled - test the connection
|
||||
// Set pending status while testing
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'pending',
|
||||
}));
|
||||
|
||||
// Test connection asynchronously (uses platform API key)
|
||||
// Test connection asynchronously - send empty body, backend will use global settings
|
||||
try {
|
||||
// Build request body based on integration type
|
||||
const requestBody: any = {};
|
||||
|
||||
// OpenAI needs model in config, Runware doesn't
|
||||
if (integrationId === 'openai') {
|
||||
requestBody.config = {
|
||||
model: model || 'gpt-4.1',
|
||||
with_response: false, // Simple connection test for status validation
|
||||
};
|
||||
}
|
||||
|
||||
const data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
// fetchAPI extracts the data field and throws on error
|
||||
// If we get here without error, validation was successful
|
||||
console.log(`✅ Validation successful for ${integrationId}`);
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'success',
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error(`Error validating ${integrationId}:`, error);
|
||||
console.error(`❌ Validation failed for ${integrationId}:`, error);
|
||||
setValidationStatuses(prev => ({
|
||||
...prev,
|
||||
[integrationId]: 'error',
|
||||
@@ -184,8 +190,8 @@ export default function Integration() {
|
||||
const validateEnabledIntegrations = useCallback(async () => {
|
||||
// Use functional update to read latest state without adding dependencies
|
||||
setIntegrations((currentIntegrations) => {
|
||||
// Validate each integration
|
||||
['openai', 'runware'].forEach((id) => {
|
||||
// Validate each integration (including image_generation)
|
||||
['openai', 'runware', 'image_generation'].forEach((id) => {
|
||||
const integration = currentIntegrations[id];
|
||||
if (!integration) return;
|
||||
|
||||
@@ -193,7 +199,7 @@ export default function Integration() {
|
||||
const model = integration.model;
|
||||
|
||||
// Validate with current state (fire and forget - don't await)
|
||||
validateIntegration(id, enabled, model);
|
||||
validateIntegration(id, enabled, undefined, model);
|
||||
});
|
||||
|
||||
// Return unchanged - we're just reading state
|
||||
@@ -201,9 +207,23 @@ export default function Integration() {
|
||||
});
|
||||
}, [validateIntegration]);
|
||||
|
||||
// Load integration settings on mount
|
||||
// Load available models from backend
|
||||
const loadAvailableModels = async () => {
|
||||
try {
|
||||
const data = await fetchAPI('/v1/system/settings/integrations/available-models/');
|
||||
if (data) {
|
||||
setAvailableModels(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading available models:', error);
|
||||
// Keep default empty arrays
|
||||
}
|
||||
};
|
||||
|
||||
// Load integration settings and available models on mount
|
||||
useEffect(() => {
|
||||
loadIntegrationSettings();
|
||||
loadAvailableModels();
|
||||
}, []);
|
||||
|
||||
// Validate integrations after settings are loaded or changed (debounced to prevent excessive validation)
|
||||
@@ -221,7 +241,7 @@ export default function Integration() {
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [integrations.openai.enabled, integrations.runware.enabled]);
|
||||
}, [integrations.openai.enabled, integrations.runware.enabled, integrations.openai.model, integrations.runware.model]);
|
||||
|
||||
const loadIntegrationSettings = async () => {
|
||||
try {
|
||||
@@ -300,12 +320,12 @@ export default function Integration() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Test uses platform API key (no apiKey parameter needed)
|
||||
// fetchAPI extracts data from unified format {success: true, data: {...}}
|
||||
// So data is the extracted response payload
|
||||
const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
apiKey,
|
||||
config: config,
|
||||
}),
|
||||
});
|
||||
@@ -411,24 +431,22 @@ export default function Integration() {
|
||||
if (integrationId === 'openai') {
|
||||
return [
|
||||
{ label: 'App Name', value: 'OpenAI API' },
|
||||
{ label: 'Model', value: config.model || 'gpt-4o-mini' },
|
||||
{ label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' },
|
||||
{ label: 'Model', value: config.model || 'Not set' },
|
||||
];
|
||||
} else if (integrationId === 'runware') {
|
||||
return [
|
||||
{ label: 'App Name', value: 'Runware API' },
|
||||
{ label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' },
|
||||
{ label: 'Model', value: config.model || 'Not set' },
|
||||
];
|
||||
} else if (integrationId === 'image_generation') {
|
||||
const service = config.service || 'openai';
|
||||
const modelDisplay = service === 'openai'
|
||||
? (config.model || config.imageModel || 'dall-e-3')
|
||||
: (config.runwareModel || 'runware:97@1');
|
||||
? (config.model || 'Not set')
|
||||
: (config.runwareModel || 'Not set');
|
||||
|
||||
return [
|
||||
{ label: 'Service', value: service === 'openai' ? 'OpenAI DALL-E' : 'Runware' },
|
||||
{ label: 'Service', value: service === 'openai' ? 'OpenAI' : 'Runware' },
|
||||
{ label: 'Model', value: modelDisplay },
|
||||
{ label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
@@ -470,25 +488,45 @@ export default function Integration() {
|
||||
key: 'model',
|
||||
label: 'AI Model',
|
||||
type: 'select',
|
||||
value: config.model || 'gpt-4.1',
|
||||
value: config.model || 'gpt-4o-mini',
|
||||
onChange: (value) => {
|
||||
setIntegrations({
|
||||
...integrations,
|
||||
[integrationId]: { ...config, model: value },
|
||||
});
|
||||
},
|
||||
options: [
|
||||
{ value: 'gpt-4.1', label: 'GPT-4.1 - $2.00 / $8.00 per 1M tokens' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o mini - $0.15 / $0.60 per 1M tokens' },
|
||||
{ value: 'gpt-4o', label: 'GPT-4o - $2.50 / $10.00 per 1M tokens' },
|
||||
{ value: 'gpt-5.1', label: 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)' },
|
||||
{ value: 'gpt-5.2', label: 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)' },
|
||||
],
|
||||
options: availableModels?.openai_text?.length > 0
|
||||
? availableModels.openai_text
|
||||
: [
|
||||
{ value: 'gpt-4.1', label: 'GPT-4.1 - $2.00 / $8.00 per 1M tokens' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o mini - $0.15 / $0.60 per 1M tokens' },
|
||||
{ value: 'gpt-4o', label: 'GPT-4o - $2.50 / $10.00 per 1M tokens' },
|
||||
{ value: 'gpt-5.1', label: 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)' },
|
||||
{ value: 'gpt-5.2', label: 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
} else if (integrationId === 'runware') {
|
||||
return [
|
||||
// Runware doesn't have model selection, just using platform API key
|
||||
{
|
||||
key: 'model',
|
||||
label: 'Runware Model',
|
||||
type: 'select',
|
||||
value: config.model || 'runware:97@1',
|
||||
onChange: (value) => {
|
||||
setIntegrations({
|
||||
...integrations,
|
||||
[integrationId]: { ...config, model: value },
|
||||
});
|
||||
},
|
||||
options: availableModels?.runware_image?.length > 0
|
||||
? availableModels.runware_image
|
||||
: [
|
||||
{ value: 'runware:97@1', label: 'Runware 97@1 - Versatile Model' },
|
||||
{ value: 'runware:100@1', label: 'Runware 100@1 - High Quality' },
|
||||
{ value: 'runware:101@1', label: 'Runware 101@1 - Fast Generation' },
|
||||
],
|
||||
},
|
||||
];
|
||||
} else if (integrationId === 'image_generation') {
|
||||
const service = config.service || 'openai';
|
||||
@@ -531,13 +569,12 @@ export default function Integration() {
|
||||
[integrationId]: { ...config, model: value },
|
||||
});
|
||||
},
|
||||
options: [
|
||||
{ value: 'dall-e-3', label: 'DALL·E 3 - $0.040 per image' },
|
||||
{ value: 'dall-e-2', label: 'DALL·E 2 - $0.020 per image' },
|
||||
// Note: gpt-image-1 and gpt-image-1-mini are not valid for OpenAI's /v1/images/generations endpoint
|
||||
// They are not currently supported by OpenAI's image generation API
|
||||
// Only dall-e-3 and dall-e-2 are supported
|
||||
],
|
||||
options: availableModels?.openai_image?.length > 0
|
||||
? availableModels.openai_image
|
||||
: [
|
||||
{ value: 'dall-e-3', label: 'DALL·E 3 - $0.040 per image' },
|
||||
{ value: 'dall-e-2', label: 'DALL·E 2 - $0.020 per image' },
|
||||
],
|
||||
});
|
||||
} else if (service === 'runware') {
|
||||
fields.push({
|
||||
@@ -551,11 +588,13 @@ export default function Integration() {
|
||||
[integrationId]: { ...config, runwareModel: value },
|
||||
});
|
||||
},
|
||||
options: [
|
||||
{ value: 'runware:97@1', label: 'HiDream-I1 Full - $0.009 per image' },
|
||||
{ value: 'runware:gen3a_turbo', label: 'Gen3a Turbo - $0.009 per image' },
|
||||
{ value: 'runware:gen3a', label: 'Gen3a - $0.009 per image' },
|
||||
],
|
||||
options: availableModels?.runware_image?.length > 0
|
||||
? availableModels.runware_image
|
||||
: [
|
||||
{ value: 'runware:97@1', label: 'HiDream-I1 Full - $0.009 per image' },
|
||||
{ value: 'runware:100@1', label: 'Runware 100@1 - High Quality' },
|
||||
{ value: 'runware:101@1', label: 'Runware 101@1 - Fast Generation' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -867,20 +906,13 @@ export default function Integration() {
|
||||
console.error('Error rendering image generation form:', error);
|
||||
return <div className="text-error-500">Error loading form. Please refresh the page.</div>;
|
||||
}
|
||||
}, [selectedIntegration, integrations, showSettingsModal, getSettingsFields]);
|
||||
}, [selectedIntegration, integrations, showSettingsModal, availableModels]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="API Integration - IGNY8" description="External integrations" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Platform API Keys Info */}
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Platform API Keys"
|
||||
message="API keys are managed at the platform level by administrators. You can customize which AI models and parameters to use for your account. Free plan users can view settings but cannot customize them."
|
||||
/>
|
||||
|
||||
{/* Integration Cards with Validation Cards */}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{/* OpenAI Integration + Validation */}
|
||||
@@ -893,10 +925,12 @@ export default function Integration() {
|
||||
integrationId="openai"
|
||||
onToggleSuccess={(enabled, data) => {
|
||||
// Refresh status circle when toggle changes
|
||||
// Use API key from hook's data (most up-to-date) or fallback to integrations state
|
||||
const apiKey = data?.apiKey || integrations.openai.apiKey;
|
||||
const model = data?.model || integrations.openai.model;
|
||||
|
||||
// Validate with current enabled state and model
|
||||
validateIntegration('openai', enabled, model);
|
||||
// Validate with current enabled state and API key
|
||||
validateIntegration('openai', enabled, apiKey, model);
|
||||
}}
|
||||
onSettings={() => handleSettings('openai')}
|
||||
onDetails={() => handleDetails('openai')}
|
||||
@@ -918,22 +952,25 @@ export default function Integration() {
|
||||
validationStatus={validationStatuses.runware}
|
||||
integrationId="runware"
|
||||
modelName={
|
||||
integrations.image_generation?.service === 'runware' && integrations.image_generation.runwareModel
|
||||
integrations.runware?.enabled && integrations.runware?.model
|
||||
? (() => {
|
||||
// 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',
|
||||
'runware:100@1': 'Runware 100@1',
|
||||
'runware:101@1': 'Runware 101@1',
|
||||
};
|
||||
return modelDisplayNames[integrations.image_generation.runwareModel] || integrations.image_generation.runwareModel;
|
||||
return modelDisplayNames[integrations.runware.model] || integrations.runware.model;
|
||||
})()
|
||||
: undefined
|
||||
}
|
||||
onToggleSuccess={(enabled, data) => {
|
||||
// Refresh status circle when toggle changes
|
||||
// Validate with current enabled state
|
||||
validateIntegration('runware', enabled);
|
||||
// Use API key from hook's data (most up-to-date) or fallback to integrations state
|
||||
const apiKey = data?.apiKey || integrations.runware.apiKey;
|
||||
|
||||
// Validate with current enabled state and API key
|
||||
validateIntegration('runware', enabled, apiKey);
|
||||
}}
|
||||
onSettings={() => handleSettings('runware')}
|
||||
onDetails={() => handleDetails('runware')}
|
||||
@@ -960,6 +997,12 @@ export default function Integration() {
|
||||
title="Image Generation Service"
|
||||
description="Default image generation service and model selection for app-wide use"
|
||||
validationStatus={validationStatuses.image_generation}
|
||||
onToggleSuccess={(enabled, data) => {
|
||||
// Validate when toggle changes - same pattern as openai/runware
|
||||
const provider = data?.provider || data?.service || 'openai';
|
||||
const model = data?.model || (provider === 'openai' ? 'dall-e-3' : 'runware:97@1');
|
||||
validateIntegration('image_generation', enabled, null, model);
|
||||
}}
|
||||
onSettings={() => handleSettings('image_generation')}
|
||||
onDetails={() => handleDetails('image_generation')}
|
||||
/>
|
||||
@@ -967,7 +1010,11 @@ export default function Integration() {
|
||||
<Alert
|
||||
variant="info"
|
||||
title="AI Integration & Image Generation Testing"
|
||||
message="Test your AI integrations and image generation on this page. The platform provides API keys - you can customize model preferences and parameters based on your plan. Test connections to verify everything is working correctly."
|
||||
message="Configure and test your AI integrations on this page.
|
||||
Set up OpenAI and Runware API keys, validate connections, and test image generation with different models and parameters.
|
||||
Before you start, please read the documentation for each integration.
|
||||
|
||||
Make sure to use the correct API keys and models for each integration."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1053,7 +1100,7 @@ export default function Integration() {
|
||||
onClick={() => {
|
||||
handleTestConnection();
|
||||
}}
|
||||
disabled={isTesting || isSaving}
|
||||
disabled={isTesting || isSaving || !integrations[selectedIntegration]?.apiKey}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isTesting ? 'Testing...' : 'Test Connection'}
|
||||
|
||||
Reference in New Issue
Block a user