This commit is contained in:
commit
46af290db3
100 files changed
+16663
No files matched your search
@@ -0,0 +1,471 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user