Files
igny8/frontend/src/pages/Thinker/Prompts.tsx
IGNY8 VPS (Salman) 4fe68cc271 ui frotneedn fixes
2025-11-26 06:47:23 +00:00

515 lines
20 KiB
TypeScript

import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import Button from '../../components/ui/button/Button';
import TextArea from '../../components/form/input/TextArea';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { BoltIcon, UserIcon, ShootingStarIcon, ImageIcon } from '../../icons';
import { fetchAPI } from '../../services/api';
interface PromptData {
prompt_type: string;
prompt_type_display: string;
prompt_value: string;
default_prompt: string;
is_active: boolean;
}
const PROMPT_TYPES = [
{
key: 'clustering',
label: 'Clustering Prompt',
description: 'Group keywords into topic clusters. Use [IGNY8_KEYWORDS] to inject keyword data.',
icon: '🌐',
color: 'green',
},
{
key: 'ideas',
label: 'Ideas Generation Prompt',
description: 'Generate content ideas from clusters. Use [IGNY8_CLUSTERS] and [IGNY8_CLUSTER_KEYWORDS] to inject data.',
icon: '💡',
color: 'amber',
},
{
key: 'content_generation',
label: 'Content Generation Prompt',
description: 'Generate content from ideas. Use [IGNY8_IDEA], [IGNY8_CLUSTER], and [IGNY8_KEYWORDS] to inject data.',
icon: '📝',
color: 'blue',
},
{
key: 'image_prompt_extraction',
label: 'Image Prompt Extraction',
description: 'Extract image prompts from article content. Use {title}, {content}, {max_images} placeholders.',
icon: '🔍',
color: 'indigo',
},
{
key: 'image_prompt_template',
label: 'Image Prompt Template',
description: 'Template for generating image prompts. Use {post_title}, {image_prompt}, {image_type} placeholders.',
icon: '🖼️',
color: 'purple',
},
{
key: 'negative_prompt',
label: 'Negative Prompt',
description: 'Specify elements to avoid in generated images (text, watermarks, logos, etc.).',
icon: '🚫',
color: 'red',
},
{
key: 'site_structure_generation',
label: 'Site Structure Generation',
description: 'Generate site structure from business brief. Use [IGNY8_BUSINESS_BRIEF], [IGNY8_OBJECTIVES], [IGNY8_STYLE], and [IGNY8_SITE_INFO] to inject data.',
icon: '🏗️',
color: 'teal',
},
];
export default function Prompts() {
const toast = useToast();
const [prompts, setPrompts] = useState<Record<string, PromptData>>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState<Record<string, boolean>>({});
// Load all prompts
useEffect(() => {
loadPrompts();
}, []);
const loadPrompts = async () => {
setLoading(true);
try {
const promises = PROMPT_TYPES.map(async (type) => {
try {
// fetchAPI extracts data from unified format {success: true, data: {...}}
// So response IS the data object
const response = await fetchAPI(`/v1/system/prompts/by_type/${type.key}/`);
return { key: type.key, data: response };
} catch (error) {
console.error(`Error loading prompt ${type.key}:`, error);
return { key: type.key, data: null };
}
});
const results = await Promise.all(promises);
const promptsMap: Record<string, PromptData> = {};
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,
};
}
});
setPrompts(promptsMap);
} catch (error: any) {
console.error('Error loading prompts:', error);
toast.error('Failed to load prompts');
} finally {
setLoading(false);
}
};
const handleSave = async (promptType: string) => {
const prompt = prompts[promptType];
if (!prompt) return;
setSaving({ ...saving, [promptType]: true });
try {
// fetchAPI extracts data from unified format {success: true, data: {...}, message: "..."}
// But save endpoint returns message in the response, so we need to check if it's still wrapped
// For now, assume success if no error is thrown
await fetchAPI('/v1/system/prompts/save/', {
method: 'POST',
body: JSON.stringify({
prompt_type: promptType,
prompt_value: prompt.prompt_value,
}),
});
toast.success('Prompt saved successfully');
await loadPrompts(); // Reload to get updated data
} catch (error: any) {
console.error('Error saving prompt:', error);
toast.error(`Failed to save prompt: ${error.message}`);
} finally {
setSaving({ ...saving, [promptType]: false });
}
};
const handleReset = async (promptType: string) => {
if (!confirm('Are you sure you want to reset this prompt to default? This will overwrite any custom changes.')) {
return;
}
setSaving({ ...saving, [promptType]: true });
try {
// fetchAPI extracts data from unified format {success: true, data: {...}, message: "..."}
// But reset endpoint returns message in the response, so we need to check if it's still wrapped
// For now, assume success if no error is thrown
await fetchAPI('/v1/system/prompts/reset/', {
method: 'POST',
body: JSON.stringify({
prompt_type: promptType,
}),
});
toast.success('Prompt reset to default');
await loadPrompts(); // Reload to get default value
} catch (error: any) {
console.error('Error resetting prompt:', error);
toast.error(`Failed to reset prompt: ${error.message}`);
} finally {
setSaving({ ...saving, [promptType]: false });
}
};
const handlePromptChange = (promptType: string, value: string) => {
setPrompts({
...prompts,
[promptType]: {
...prompts[promptType],
prompt_value: value,
},
});
};
if (loading) {
return (
<>
<PageMeta title="Prompts - IGNY8" description="AI prompts management" />
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading prompts...</p>
</div>
</div>
</>
);
}
// Thinker navigation tabs
const thinkerTabs = [
{ label: 'Prompts', path: '/thinker/prompts', icon: <BoltIcon /> },
{ label: 'Author Profiles', path: '/thinker/author-profiles', icon: <UserIcon /> },
{ label: 'Strategies', path: '/thinker/strategies', icon: <ShootingStarIcon /> },
{ label: 'Image Testing', path: '/thinker/image-testing', icon: <ImageIcon /> },
];
return (
<>
<PageMeta title="Prompts - IGNY8" description="AI prompts management" />
<PageHeader
title="AI Prompts Management"
badge={{ icon: <BoltIcon />, color: 'orange' }}
navigation={<ModuleNavigationTabs tabs={thinkerTabs} />}
/>
<div className="p-6">
{/* Planner Prompts Section */}
<div className="mb-8">
<div className="mb-4">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
Planner Prompts
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI prompt templates for clustering and idea generation
</p>
</div>
{/* 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) => {
const prompt = prompts[type.key] || {
prompt_type: type.key,
prompt_type_display: type.label,
prompt_value: '',
default_prompt: '',
is_active: true,
};
return (
<div key={type.key} className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
<div className="p-5 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{type.icon}</span>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{type.label}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{type.description}
</p>
</div>
</div>
</div>
</div>
<div className="p-5">
<TextArea
value={prompt.prompt_value || ''}
onChange={(value) => handlePromptChange(type.key, value)}
rows={12}
placeholder="Enter prompt template..."
className="font-mono-custom text-sm"
/>
<div className="flex justify-end gap-3 mt-4">
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</Button>
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
variant="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Writer Prompts Section */}
<div className="mb-8">
<div className="mb-4">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
Writer Prompts
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI prompt templates for content writing
</p>
</div>
{/* 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) => {
const prompt = prompts[type.key] || {
prompt_type: type.key,
prompt_type_display: type.label,
prompt_value: '',
default_prompt: '',
is_active: true,
};
return (
<div key={type.key}>
<div className="p-5 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{type.icon}</span>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{type.label}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{type.description}
</p>
</div>
</div>
</div>
</div>
<div className="p-5">
<TextArea
value={prompt.prompt_value || ''}
onChange={(value) => handlePromptChange(type.key, value)}
rows={15}
placeholder="Enter prompt template..."
className="font-mono-custom text-sm"
/>
<div className="flex justify-end gap-3 mt-4">
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</Button>
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
variant="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Image Generation Section */}
<div className="mb-8">
<div className="mb-4">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
Image Generation
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI image generation prompts
</p>
</div>
{/* 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) => {
const prompt = prompts[type.key] || {
prompt_type: type.key,
prompt_type_display: type.label,
prompt_value: '',
default_prompt: '',
is_active: true,
};
return (
<div key={type.key} className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
<div className="p-5 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{type.icon}</span>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{type.label}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{type.description}
</p>
</div>
</div>
</div>
</div>
<div className="p-5">
<TextArea
value={prompt.prompt_value || ''}
onChange={(value) => handlePromptChange(type.key, value)}
rows={type.key === 'negative_prompt' ? 4 : 8}
placeholder="Enter prompt template..."
className="font-mono-custom text-sm"
/>
<div className="flex justify-end gap-3 mt-4">
{type.key === 'image_prompt_template' && (
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</Button>
)}
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
variant="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Site Builder Prompts Section */}
<div className="mb-8">
<div className="mb-4">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
Site Builder
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure AI prompt templates for site structure generation
</p>
</div>
{/* 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) => {
const prompt = prompts[type.key] || {
prompt_type: type.key,
prompt_type_display: type.label,
prompt_value: '',
default_prompt: '',
is_active: true,
};
return (
<div key={type.key}>
<div className="p-5 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{type.icon}</span>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{type.label}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{type.description}
</p>
<div className="mt-2 text-xs text-gray-500 dark:text-gray-500">
<p className="font-semibold mb-1">Available Variables:</p>
<ul className="list-disc list-inside space-y-1">
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_BUSINESS_BRIEF]</code> - Business description and context</li>
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_OBJECTIVES]</code> - Site objectives and goals</li>
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_STYLE]</code> - Design style preferences</li>
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_SITE_INFO]</code> - Site type and requirements</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div className="p-5">
<TextArea
value={prompt.prompt_value || ''}
onChange={(value) => handlePromptChange(type.key, value)}
rows={15}
placeholder="Enter prompt template for site structure generation..."
className="font-mono-custom text-sm"
/>
<div className="flex justify-end gap-3 mt-4">
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</Button>
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
variant="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</>
);
}