515 lines
20 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|