472 lines
13 KiB
TypeScript
472 lines
13 KiB
TypeScript
import axios from 'axios';
|
|
import type { DocumentNode, DocumentTreeNode, ModelSettings } from '@/stores/appStore';
|
|
|
|
// Конфигурация API
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
|
|
|
|
// Типы
|
|
export interface User {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
}
|
|
|
|
export interface AuthResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
user_id: string;
|
|
username: string;
|
|
}
|
|
|
|
// Типы для конфигурации моделей
|
|
export interface ModelInfo {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
context_length?: number;
|
|
pricing?: string;
|
|
}
|
|
|
|
export interface ModelsResponse {
|
|
provider: string;
|
|
default_model: string;
|
|
models: ModelInfo[];
|
|
}
|
|
|
|
export type Provider = 'ollama' | 'openrouter';
|
|
|
|
// Типы для Project
|
|
export interface Project {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
// Типы для Story Bible
|
|
export interface Character {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
traits: string[];
|
|
appearance: string;
|
|
role: 'protagonist' | 'antagonist' | 'supporting' | 'minor';
|
|
}
|
|
|
|
export interface Location {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
atmosphere: string;
|
|
type: 'city' | 'building' | 'nature' | 'interior' | 'other';
|
|
}
|
|
|
|
export interface Event {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
chapter?: string;
|
|
importance: 'major' | 'minor' | 'background';
|
|
}
|
|
|
|
export interface StoryThread {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
status: 'active' | 'resolved' | 'abandoned';
|
|
related_characters: string[];
|
|
related_events: string[];
|
|
}
|
|
|
|
export interface StoryBible {
|
|
characters: Character[];
|
|
locations: Location[];
|
|
events: Event[];
|
|
threads: StoryThread[];
|
|
last_updated?: string;
|
|
}
|
|
|
|
export interface CleanupDuplicatesResponse {
|
|
removed_characters: number;
|
|
removed_locations: number;
|
|
removed_events: number;
|
|
message: string;
|
|
}
|
|
|
|
// Экспортируем все типы Story Bible
|
|
export type { Character, Location, Event, StoryThread, StoryBible };
|
|
|
|
const apiClient = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Интерсептор для автоматического добавления токена
|
|
apiClient.interceptors.request.use((config) => {
|
|
const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
|
|
if (token && config.headers) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
|
|
interface ApiProjectResponse {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
interface ApiProjectCreate {
|
|
title: string;
|
|
description: string;
|
|
}
|
|
|
|
|
|
// API функции для работы с проектами
|
|
export const projectsApi = {
|
|
/**
|
|
* Получить все проекты
|
|
*/
|
|
async getAllProjects(): Promise<Project[]> {
|
|
const response = await apiClient.get<ApiProjectResponse[]>('/projects');
|
|
return response.data.map((p) => ({
|
|
...p,
|
|
created_at: p.created_at,
|
|
updated_at: p.updated_at,
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Создать новый проект
|
|
*/
|
|
async createProject(project: Omit<ApiProjectCreate, 'id'>): Promise<Project> {
|
|
const response = await apiClient.post<ApiProjectResponse>('/projects', project);
|
|
return {
|
|
...response.data,
|
|
created_at: response.data.created_at,
|
|
updated_at: response.data.updated_at,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Обновить проект
|
|
*/
|
|
async updateProject(
|
|
projectId: string,
|
|
updates: Partial<Omit<ApiProjectCreate, 'id'>>
|
|
): Promise<Project> {
|
|
const response = await apiClient.put<ApiProjectResponse>(`/projects/${projectId}`, updates);
|
|
return {
|
|
...response.data,
|
|
created_at: response.data.created_at,
|
|
updated_at: response.data.updated_at,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Удалить проект
|
|
*/
|
|
async deleteProject(projectId: string): Promise<void> {
|
|
await apiClient.delete(`/projects/${projectId}`);
|
|
},
|
|
};
|
|
|
|
// API функции для работы с документами (иерархические)
|
|
export const documentsApi = {
|
|
/**
|
|
* Получить документы в виде дерева
|
|
*/
|
|
async getDocumentsTree(projectId: string): Promise<DocumentTreeNode[]> {
|
|
const response = await apiClient.get<DocumentTreeNode[]>(
|
|
`/projects/${projectId}/documents/tree`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Получить все документы (плоский список)
|
|
*/
|
|
async getProjectDocuments(projectId: string): Promise<DocumentNode[]> {
|
|
const response = await apiClient.get<DocumentNode[]>(
|
|
`/projects/${projectId}/documents`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Получить документ по ID
|
|
*/
|
|
async getDocument(projectId: string, documentId: string): Promise<DocumentNode> {
|
|
const response = await apiClient.get<DocumentNode>(
|
|
`/projects/${projectId}/documents/${documentId}`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Создать документ в проекте
|
|
*/
|
|
async createDocument(
|
|
projectId: string,
|
|
title: string,
|
|
type: 'book' | 'chapter' | 'scene',
|
|
parentId?: string,
|
|
content: string = ''
|
|
): Promise<DocumentNode> {
|
|
const response = await apiClient.post<DocumentNode>(
|
|
`/projects/${projectId}/documents`,
|
|
{
|
|
title,
|
|
type,
|
|
parent_id: parentId,
|
|
content,
|
|
}
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Обновить документ (включая перенос)
|
|
*/
|
|
async updateDocument(
|
|
projectId: string,
|
|
documentId: string,
|
|
updates: {
|
|
title?: string;
|
|
content?: string;
|
|
parent_id?: string;
|
|
order?: number;
|
|
}
|
|
): Promise<DocumentNode> {
|
|
const response = await apiClient.put<DocumentNode>(
|
|
`/projects/${projectId}/documents/${documentId}`,
|
|
updates
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Удалить документ с каскадом
|
|
*/
|
|
async deleteDocument(projectId: string, documentId: string): Promise<void> {
|
|
await apiClient.delete(`/projects/${projectId}/documents/${documentId}`);
|
|
},
|
|
|
|
/**
|
|
* Получить прямых детей узла
|
|
*/
|
|
async getChildren(projectId: string, parentId: string): Promise<DocumentNode[]> {
|
|
const response = await apiClient.get<DocumentNode[]>(
|
|
`/projects/${projectId}/documents/${parentId}/children`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
// --- Legacy функции для обратной совместимости ---
|
|
|
|
/**
|
|
* Обновить контент документа (legacy)
|
|
*/
|
|
async updateDocumentContent(
|
|
projectId: string,
|
|
documentId: string,
|
|
content: string
|
|
): Promise<DocumentNode> {
|
|
const updatedDoc = await documentsApi.updateDocument(projectId, documentId, {
|
|
content,
|
|
});
|
|
return updatedDoc;
|
|
},
|
|
};
|
|
|
|
// API функции для Story Bible
|
|
export const storyBibleApi = {
|
|
/**
|
|
* Получить Story Bible проекта
|
|
*/
|
|
async getProjectStoryBible(projectId: string): Promise<StoryBible> {
|
|
const response = await apiClient.get<StoryBible>(`/projects/${projectId}/bible`);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Обновить Story Bible проекта
|
|
*/
|
|
async updateProjectStoryBible(
|
|
projectId: string,
|
|
bible: StoryBible
|
|
): Promise<StoryBible> {
|
|
const response = await apiClient.put<StoryBible>(`/projects/${projectId}/bible`, bible);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* Очистить дубликаты в Story Bible
|
|
*/
|
|
async cleanupDuplicates(projectId: string): Promise<CleanupDuplicatesResponse> {
|
|
const response = await apiClient.post<CleanupDuplicatesResponse>(
|
|
`/projects/${projectId}/bible/cleanup-duplicates`
|
|
);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
const apiClientFactory = () => apiClient;
|
|
|
|
export default apiClientFactory;
|
|
|
|
/**
|
|
* Экспорт документа в выбранный формат (epub, fb2, pdf, docx)
|
|
*/
|
|
export async function exportDocument(
|
|
projectId: string,
|
|
documentId: string,
|
|
format: 'epub' | 'fb2' | 'pdf' | 'docx'
|
|
): Promise<void> {
|
|
try {
|
|
const response = await apiClient.post(`/export`,
|
|
{ project_id: projectId, document_id: documentId, format },
|
|
{ responseType: 'blob' }
|
|
);
|
|
|
|
// Создаем временную ссылку для скачивания
|
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
|
|
// Определяем расширение файла
|
|
const extension = format === 'docx' ? 'docx' : format;
|
|
link.setAttribute('download', `export.${extension}`);
|
|
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw new Error(`Export failed: ${error.response?.data?.detail || error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить список доступных моделей для провайдера
|
|
*/
|
|
export async function getAvailableModels(provider: Provider): Promise<ModelsResponse> {
|
|
try {
|
|
const response = await apiClient.get<ModelsResponse>(`/ai/models?provider=${provider}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw new Error(`Failed to fetch models: ${error.response?.data?.detail || error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить данные модели для конкретного endpoint (generation, background, analysis)
|
|
*/
|
|
export async function getModelForTask(taskType: keyof ModelSettings): Promise<ModelsResponse> {
|
|
try {
|
|
// Получаем настройки из localStorage
|
|
if (typeof window !== 'undefined') {
|
|
const savedSettings = localStorage.getItem('modelSettings');
|
|
if (savedSettings) {
|
|
const settings: ModelSettings = JSON.parse(savedSettings);
|
|
const modelSetting = settings[taskType];
|
|
if (modelSetting) {
|
|
const response = await apiClient.get<ModelsResponse>(
|
|
`/ai/models?provider=${modelSetting.provider}`
|
|
);
|
|
// Обновляем default_model на выбранный пользователем
|
|
return {
|
|
...response.data,
|
|
default_model: modelSetting.model,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Если нет настроек, используем дефолтные
|
|
const response = await apiClient.get<ModelsResponse>(`/ai/models?provider=ollama`);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw new Error(`Failed to fetch model for ${taskType}: ${error.response?.data?.detail || error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// === ФУНКЦИИ ГЕНЕРАЦИИ ИЗОБРАЖЕНИЙ ===
|
|
|
|
export interface GenerateImageRequest {
|
|
prompt: string;
|
|
project_id: string;
|
|
document_id: string; // ID документа (книги или главы)
|
|
document_type: 'book' | 'chapter'; // Тип документа
|
|
model?: string;
|
|
size?: string;
|
|
}
|
|
|
|
export interface GenerateImageResponse {
|
|
image_id: string;
|
|
filename: string;
|
|
path: string;
|
|
prompt: string;
|
|
model: string;
|
|
data_url: string; // base64 изображение для немедленного отображения
|
|
document_id: string;
|
|
document_type: string;
|
|
}
|
|
|
|
/**
|
|
* Сгенерировать изображение
|
|
*/
|
|
export async function generateImage(request: GenerateImageRequest): Promise<GenerateImageResponse> {
|
|
const response = await apiClient.post<GenerateImageResponse>('/ai/generate-image', request);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Получить URL изображения
|
|
*/
|
|
export function getImageUrl(projectId: string, filename: string): string {
|
|
const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace('/api', '') || 'http://localhost:8000';
|
|
return `${baseUrl}/api/images/${projectId}/${filename}`;
|
|
}
|
|
|
|
// === ФУНКЦИИ АВТОРИЗАЦИИ ===
|
|
|
|
/**
|
|
* Войти в систему
|
|
*/
|
|
export async function login(username: string, password: string): Promise<AuthResponse> {
|
|
const response = await apiClient.post<AuthResponse>('/auth/login', { username, password });
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Зарегистрировать нового пользователя
|
|
*/
|
|
export async function register(username: string, email: string, password: string): Promise<AuthResponse> {
|
|
const response = await apiClient.post<AuthResponse>('/auth/register', { username, email, password });
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Получить текущего пользователя
|
|
*/
|
|
export async function fetchCurrentUser(): Promise<User> {
|
|
const response = await apiClient.get<User>('/auth/me');
|
|
return response.data;
|
|
}
|