Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAuthorProfiles, createAuthorProfile, updateAuthorProfile, deleteAuthorProfile, AuthorProfile } from '../../services/api';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import FormModal, { FormField } from '../../components/common/FormModal';
import Badge from '../../components/ui/badge/Badge';
import { PlusIcon } from '../../icons';
export default function AuthorProfiles() {
const toast = useToast();
const [profiles, setProfiles] = useState<AuthorProfile[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProfile, setEditingProfile] = useState<AuthorProfile | null>(null);
const [formData, setFormData] = useState({
name: '',
description: '',
tone: '',
language: 'en',
is_active: true,
});
useEffect(() => {
loadProfiles();
}, []);
const loadProfiles = async () => {
try {
setLoading(true);
const response = await fetchAuthorProfiles();
setProfiles(response.results || []);
} catch (error: any) {
toast.error(`Failed to load author profiles: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleCreate = () => {
setEditingProfile(null);
setFormData({
name: '',
description: '',
tone: '',
language: 'en',
is_active: true,
});
setIsModalOpen(true);
};
const handleEdit = (profile: AuthorProfile) => {
setEditingProfile(profile);
setFormData({
name: profile.name,
description: profile.description,
tone: profile.tone,
language: profile.language,
is_active: profile.is_active,
});
setIsModalOpen(true);
};
const handleSave = async () => {
try {
if (editingProfile) {
await updateAuthorProfile(editingProfile.id, formData);
toast.success('Author profile updated successfully');
} else {
await createAuthorProfile(formData);
toast.success('Author profile created successfully');
}
setIsModalOpen(false);
loadProfiles();
} catch (error: any) {
toast.error(`Failed to save: ${error.message}`);
}
};
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this author profile?')) return;
try {
await deleteAuthorProfile(id);
toast.success('Author profile deleted successfully');
loadProfiles();
} catch (error: any) {
toast.error(`Failed to delete: ${error.message}`);
}
};
const formFields: FormField[] = [
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'description', label: 'Description', type: 'textarea', required: false },
{ name: 'tone', label: 'Tone', type: 'text', required: true },
{ name: 'language', label: 'Language', type: 'text', required: true },
{ name: 'is_active', label: 'Active', type: 'checkbox', required: false },
];
return (
<div className="p-6">
<PageMeta title="Author Profiles" />
<div className="mb-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Author Profiles</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage writing style profiles</p>
</div>
<Button onClick={handleCreate} variant="primary">
<PlusIcon className="w-4 h-4 mr-2" />
Create Profile
</Button>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{profiles.map((profile) => (
<Card key={profile.id} className="p-6">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{profile.name}</h3>
<Badge variant="light" color={profile.is_active ? 'success' : 'dark'}>
{profile.is_active ? 'Active' : 'Inactive'}
</Badge>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{profile.description}</p>
<div className="space-y-2 mb-4">
<div className="text-sm">
<span className="text-gray-500 dark:text-gray-400">Tone:</span>{' '}
<span className="text-gray-900 dark:text-white">{profile.tone}</span>
</div>
<div className="text-sm">
<span className="text-gray-500 dark:text-gray-400">Language:</span>{' '}
<span className="text-gray-900 dark:text-white">{profile.language}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="secondary" size="sm" onClick={() => handleEdit(profile)}>
Edit
</Button>
<Button variant="danger" size="sm" onClick={() => handleDelete(profile.id)}>
Delete
</Button>
</div>
</Card>
))}
</div>
)}
<FormModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleSave}
title={editingProfile ? 'Edit Author Profile' : 'Create Author Profile'}
fields={formFields}
data={formData}
onChange={setFormData}
/>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
export default function ThinkerDashboard() {
return (
<>
<PageMeta title="Thinker Dashboard - IGNY8" description="AI thinker overview" />
<ComponentCard title="Coming Soon" desc="AI thinker overview">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Thinker Dashboard - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Overview of AI tools and strategies will be displayed here
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -0,0 +1,22 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
export default function ImageTesting() {
return (
<>
<PageMeta title="Image Testing - IGNY8" description="AI image testing" />
<ComponentCard title="Coming Soon" desc="AI image testing">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Image Testing - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Test and configure AI image generation capabilities
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -0,0 +1,22 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
export default function Profile() {
return (
<>
<PageMeta title="AI Profile - IGNY8" description="AI profile settings" />
<ComponentCard title="Coming Soon" desc="AI profile settings">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
AI Profile Settings - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Configure AI personality and writing style
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -0,0 +1,432 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import Button from '../../components/ui/button/Button';
import TextArea from '../../components/form/input/TextArea';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { BoltIcon } 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',
},
];
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 {
const data = await fetchAPI(`/v1/system/prompts/by_type/${type.key}/`);
return { key: type.key, data };
} 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 {
const data = await fetchAPI('/v1/system/prompts/save/', {
method: 'POST',
body: JSON.stringify({
prompt_type: promptType,
prompt_value: prompt.prompt_value,
}),
});
if (data.success) {
toast.success(data.message || 'Prompt saved successfully');
await loadPrompts(); // Reload to get updated data
} else {
throw new Error(data.error || 'Failed to save prompt');
}
} 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 {
const data = await fetchAPI('/v1/system/prompts/reset/', {
method: 'POST',
body: JSON.stringify({
prompt_type: promptType,
}),
});
if (data.success) {
toast.success(data.message || 'Prompt reset to default');
await loadPrompts(); // Reload to get default value
} else {
throw new Error(data.error || 'Failed to reset prompt');
}
} 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>
</>
);
}
return (
<>
<PageMeta title="Prompts - IGNY8" description="AI prompts management" />
<div className="p-6">
{/* Page Header */}
<div className="mb-6">
<div className="flex items-center gap-3 mb-2">
<BoltIcon className="text-primary-500 size-6" />
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white">
AI Prompts Management
</h1>
</div>
<p className="text-gray-600 dark:text-gray-400">
Configure AI prompt templates for clustering, idea generation, content writing, and image generation
</p>
</div>
{/* 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 gap-3 mt-4">
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
className="flex-1"
variant="solid"
color="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</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 gap-3 mt-4">
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
className="flex-1"
variant="solid"
color="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</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 gap-3 mt-4">
<Button
onClick={() => handleSave(type.key)}
disabled={saving[type.key]}
className="flex-1"
variant="solid"
color="primary"
>
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
</Button>
{type.key === 'image_prompt_template' && (
<Button
onClick={() => handleReset(type.key)}
disabled={saving[type.key]}
variant="outline"
>
Reset to Default
</Button>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,22 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
export default function Strategies() {
return (
<>
<PageMeta title="Strategies - IGNY8" description="Content strategies" />
<ComponentCard title="Coming Soon" desc="Content strategies">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Content Strategies - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Plan and manage content strategies and approaches
</p>
</div>
</ComponentCard>
</>
);
}