Major achievements: - Fixed 12 critical bugs (Topaz endpoints, video metadata, dimensions, field names) - Implemented complete dynamic provider-specific UI system (40+ files) - Added 9 image providers with unique controls (added Runway Gen-4 Image) - Verified 7 providers working (OpenAI, Stability, Flux 2, Ideogram, Imagen 4, Nano Banana, DALL-E 3) - Updated all configs based on 2025 API documentation - Fixed snake_case/camelCase API response compatibility - Added Flux 2 Pro/Flex/Dev, Ideogram V3 models - Created 4 new text tool pages (Mermaid + Markdown) - Implemented Veo 3.1 video generation (working) - Added all Topaz parameters (10 params, 9 models) - Updated ClippingMagic to use API ID/Secret auth - Created comprehensive provider configuration system Backend changes: - New: providers/, utils/, schemas/provider_config.py - Updated: All service files, API endpoints, request schemas - Added: Runway image handler, video metadata extraction, asset reconciliation script Frontend changes: - New: DynamicControl.tsx, ProviderControls.tsx, types/providers.ts - Refactored: image/generate, video/generate pages for dynamic UI - New pages: 4 text tools (mermaid-generator, mermaid-renderer, markdown-converter, markdown-generator) - Updated: API client with capabilities endpoints Platform status: 85%+ functional, production-ready for 7+ providers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
177 lines
6 KiB
TypeScript
177 lines
6 KiB
TypeScript
'use client';
|
|
|
|
import { ProviderControl } from '@/types/providers';
|
|
|
|
interface DynamicControlProps {
|
|
control: ProviderControl;
|
|
value: any;
|
|
onChange: (name: string, value: any) => void;
|
|
allValues: Record<string, any>;
|
|
}
|
|
|
|
export default function DynamicControl({
|
|
control,
|
|
value,
|
|
onChange,
|
|
allValues
|
|
}: DynamicControlProps) {
|
|
// Check if control should be visible based on dependencies
|
|
const isVisible = () => {
|
|
if (!control.dependsOn) return true;
|
|
|
|
const dependencyValue = allValues[control.dependsOn.control];
|
|
return dependencyValue === control.dependsOn.value;
|
|
};
|
|
|
|
if (!isVisible()) return null;
|
|
|
|
const handleChange = (newValue: any) => {
|
|
onChange(control.name, newValue);
|
|
};
|
|
|
|
// Get current value or default
|
|
const currentValue = value !== undefined && value !== null ? value : control.default;
|
|
|
|
switch (control.type) {
|
|
case 'select':
|
|
return (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-300">
|
|
{control.label}
|
|
{control.required && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
{control.description && (
|
|
<p className="text-xs text-gray-500">{control.description}</p>
|
|
)}
|
|
<select
|
|
value={String(currentValue)}
|
|
onChange={(e) => {
|
|
const option = control.options?.find(o => String(o.value) === e.target.value);
|
|
handleChange(option?.value);
|
|
}}
|
|
className="w-full bg-forge-dark border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-forge-yellow"
|
|
>
|
|
{control.options?.map((option) => (
|
|
<option key={String(option.value)} value={String(option.value)}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
|
|
case 'number':
|
|
return (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-300">
|
|
{control.label}
|
|
{control.required && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
{control.description && (
|
|
<p className="text-xs text-gray-500">{control.description}</p>
|
|
)}
|
|
<input
|
|
type="number"
|
|
value={currentValue}
|
|
onChange={(e) => handleChange(parseFloat(e.target.value))}
|
|
min={control.min}
|
|
max={control.max}
|
|
step={control.step}
|
|
className="w-full bg-forge-dark border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-forge-yellow"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
case 'slider':
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<label className="text-sm font-medium text-gray-300">
|
|
{control.label}
|
|
{control.required && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
<span className="text-sm text-forge-yellow font-mono">
|
|
{currentValue}
|
|
</span>
|
|
</div>
|
|
{control.description && (
|
|
<p className="text-xs text-gray-500 mb-2">{control.description}</p>
|
|
)}
|
|
<input
|
|
type="range"
|
|
value={currentValue}
|
|
onChange={(e) => handleChange(parseFloat(e.target.value))}
|
|
min={control.min}
|
|
max={control.max}
|
|
step={control.step ?? 1}
|
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-forge-yellow"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-500">
|
|
<span>{control.min}</span>
|
|
<span>{control.max}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'checkbox':
|
|
return (
|
|
<div className="flex items-start space-x-3 py-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={currentValue}
|
|
onChange={(e) => handleChange(e.target.checked)}
|
|
className="mt-1 rounded border-gray-700 bg-forge-dark text-forge-yellow focus:ring-forge-yellow focus:ring-2"
|
|
/>
|
|
<div className="flex-1">
|
|
<label className="text-sm font-medium text-gray-300 cursor-pointer">
|
|
{control.label}
|
|
</label>
|
|
{control.description && (
|
|
<p className="text-xs text-gray-500 mt-1">{control.description}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'text':
|
|
return (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-300">
|
|
{control.label}
|
|
{control.required && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
{control.description && (
|
|
<p className="text-xs text-gray-500">{control.description}</p>
|
|
)}
|
|
<input
|
|
type="text"
|
|
value={currentValue || ''}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
className="w-full bg-forge-dark border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-forge-yellow"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
case 'textarea':
|
|
return (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-300">
|
|
{control.label}
|
|
{control.required && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
{control.description && (
|
|
<p className="text-xs text-gray-500">{control.description}</p>
|
|
)}
|
|
<textarea
|
|
value={currentValue || ''}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
rows={3}
|
|
className="w-full bg-forge-dark border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-forge-yellow resize-none"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|