feat: Add Global Module Settings and Caption to Images
- Introduced GlobalModuleSettings model for platform-wide module enable/disable settings. - Added 'caption' field to Images model to store image captions. - Updated GenerateImagePromptsFunction to handle new caption structure in prompts. - Enhanced AIPromptViewSet to return global prompt types and validate active prompts. - Modified serializers and views to accommodate new caption field and global settings. - Updated frontend components to display captions and filter prompts based on active types. - Created migrations for GlobalModuleSettings and added caption field to Images.
This commit is contained in:
@@ -73,6 +73,7 @@ export default function Prompts() {
|
||||
const [prompts, setPrompts] = useState<Record<string, PromptData>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState<Record<string, boolean>>({});
|
||||
const [activePromptTypes, setActivePromptTypes] = useState<string[]>([]);
|
||||
|
||||
// Load all prompts
|
||||
useEffect(() => {
|
||||
@@ -82,7 +83,15 @@ export default function Prompts() {
|
||||
const loadPrompts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const promises = PROMPT_TYPES.map(async (type) => {
|
||||
// First, get the list of globally active prompt types
|
||||
const activeTypesResponse = await fetchAPI('/v1/system/prompts/active_types/');
|
||||
const activeTypes = activeTypesResponse.active_types || [];
|
||||
setActivePromptTypes(activeTypes);
|
||||
|
||||
// Only load prompts that are globally active
|
||||
const activePromptConfigs = PROMPT_TYPES.filter(type => activeTypes.includes(type.key));
|
||||
|
||||
const promises = activePromptConfigs.map(async (type) => {
|
||||
try {
|
||||
// fetchAPI extracts data from unified format {success: true, data: {...}}
|
||||
// So response IS the data object
|
||||
@@ -100,15 +109,6 @@ export default function Prompts() {
|
||||
results.forEach(({ key, data }) => {
|
||||
if (data) {
|
||||
promptsMap[key] = data;
|
||||
} else {
|
||||
// Use default if not found
|
||||
promptsMap[key] = {
|
||||
prompt_type: key,
|
||||
prompt_type_display: PROMPT_TYPES.find(t => t.key === key)?.label || key,
|
||||
prompt_value: '',
|
||||
default_prompt: '',
|
||||
is_active: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -218,6 +218,7 @@ export default function Prompts() {
|
||||
<div className="p-6">
|
||||
|
||||
{/* Planner Prompts Section */}
|
||||
{PROMPT_TYPES.filter(t => ['clustering', 'ideas'].includes(t.key) && activePromptTypes.includes(t.key)).length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
|
||||
@@ -231,7 +232,7 @@ export default function Prompts() {
|
||||
{/* 2-Column Grid for Planner Prompts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Clustering Prompt */}
|
||||
{PROMPT_TYPES.filter(t => ['clustering', 'ideas'].includes(t.key)).map((type) => {
|
||||
{PROMPT_TYPES.filter(t => ['clustering', 'ideas'].includes(t.key) && activePromptTypes.includes(t.key)).map((type) => {
|
||||
const prompt = prompts[type.key] || {
|
||||
prompt_type: type.key,
|
||||
prompt_type_display: type.label,
|
||||
@@ -287,8 +288,10 @@ export default function Prompts() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Writer Prompts Section */}
|
||||
{PROMPT_TYPES.filter(t => t.key === 'content_generation' && activePromptTypes.includes(t.key)).length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
|
||||
@@ -301,7 +304,7 @@ export default function Prompts() {
|
||||
|
||||
{/* Content Generation Prompt */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
{PROMPT_TYPES.filter(t => t.key === 'content_generation').map((type) => {
|
||||
{PROMPT_TYPES.filter(t => t.key === 'content_generation' && activePromptTypes.includes(t.key)).map((type) => {
|
||||
const prompt = prompts[type.key] || {
|
||||
prompt_type: type.key,
|
||||
prompt_type_display: type.label,
|
||||
@@ -357,8 +360,10 @@ export default function Prompts() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image Generation Section */}
|
||||
{PROMPT_TYPES.filter(t => ['image_prompt_extraction', 'image_prompt_template', 'negative_prompt'].includes(t.key) && activePromptTypes.includes(t.key)).length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
|
||||
@@ -371,7 +376,7 @@ export default function Prompts() {
|
||||
|
||||
{/* 2-Column Grid for Image Prompts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{PROMPT_TYPES.filter(t => ['image_prompt_extraction', 'image_prompt_template', 'negative_prompt'].includes(t.key)).map((type) => {
|
||||
{PROMPT_TYPES.filter(t => ['image_prompt_extraction', 'image_prompt_template', 'negative_prompt'].includes(t.key) && activePromptTypes.includes(t.key)).map((type) => {
|
||||
const prompt = prompts[type.key] || {
|
||||
prompt_type: type.key,
|
||||
prompt_type_display: type.label,
|
||||
@@ -429,8 +434,10 @@ export default function Prompts() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Site Builder Prompts Section */}
|
||||
{PROMPT_TYPES.filter(t => t.key === 'site_structure_generation' && activePromptTypes.includes(t.key)).length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
|
||||
@@ -443,7 +450,7 @@ export default function Prompts() {
|
||||
|
||||
{/* Site Structure Generation Prompt */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
{PROMPT_TYPES.filter(t => t.key === 'site_structure_generation').map((type) => {
|
||||
{PROMPT_TYPES.filter(t => t.key === 'site_structure_generation' && activePromptTypes.includes(t.key)).map((type) => {
|
||||
const prompt = prompts[type.key] || {
|
||||
prompt_type: type.key,
|
||||
prompt_type_display: type.label,
|
||||
@@ -508,6 +515,7 @@ export default function Prompts() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1377,6 +1377,7 @@ export interface ImageRecord {
|
||||
image_url?: string | null;
|
||||
image_path?: string | null;
|
||||
prompt?: string | null;
|
||||
caption?: string | null;
|
||||
status: string;
|
||||
position: number;
|
||||
created_at: string;
|
||||
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
AccountSettingsError,
|
||||
} from '../services/api';
|
||||
|
||||
// Version for cache busting - increment when structure changes
|
||||
const SETTINGS_STORE_VERSION = 2;
|
||||
|
||||
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
|
||||
switch (error.type) {
|
||||
case 'ACCOUNT_SETTINGS_NOT_FOUND':
|
||||
@@ -241,11 +244,23 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
}),
|
||||
{
|
||||
name: 'settings-storage',
|
||||
version: SETTINGS_STORE_VERSION, // Add version for cache busting
|
||||
partialize: (state) => ({
|
||||
accountSettings: state.accountSettings,
|
||||
moduleSettings: state.moduleSettings,
|
||||
moduleEnableSettings: state.moduleEnableSettings,
|
||||
}),
|
||||
// Migrate function to handle version changes
|
||||
migrate: (persistedState: any, version: number) => {
|
||||
if (version < SETTINGS_STORE_VERSION) {
|
||||
// Clear module enable settings on version upgrade
|
||||
return {
|
||||
...persistedState,
|
||||
moduleEnableSettings: null,
|
||||
};
|
||||
}
|
||||
return persistedState;
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -275,15 +275,15 @@ const FeaturedImageBlock = ({
|
||||
) : (
|
||||
<PromptPlaceholder prompt={image?.prompt} minHeight={420} label="Featured Image Prompt" />
|
||||
)}
|
||||
{image?.prompt && imageSrc && (
|
||||
{image?.caption && imageSrc && (
|
||||
<div className="absolute bottom-5 left-5 rounded-full bg-white/80 px-4 py-2 text-xs font-medium text-slate-600 backdrop-blur-sm dark:bg-gray-950/70 dark:text-slate-300">
|
||||
Prompt aligned to hero section
|
||||
Caption aligned to hero section
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{image?.prompt && (
|
||||
{image?.caption && (
|
||||
<div className="border-t border-slate-200/70 bg-white/70 px-8 py-6 text-sm leading-relaxed text-slate-600 backdrop-blur-sm dark:border-gray-800/60 dark:bg-gray-900/70 dark:text-slate-300">
|
||||
{image.prompt}
|
||||
{image.caption}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -322,12 +322,12 @@ const SectionImageBlock = ({
|
||||
<ImageStatusPill status={image?.status} />
|
||||
</div>
|
||||
</div>
|
||||
{image?.prompt && (
|
||||
{image?.caption && (
|
||||
<figcaption className="space-y-3 px-6 py-5 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
|
||||
<p className="font-semibold uppercase tracking-[0.25em] text-slate-400 dark:text-slate-500">
|
||||
Visual Direction
|
||||
Image Caption
|
||||
</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{image.prompt}</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{image.caption}</p>
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
|
||||
Reference in New Issue
Block a user