Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { emitApiEvent } from '../utils/apiEvents';
|
||||
import { getApiUrl } from '../api/client';
|
||||
import { getApiUrl, getAuthTokenGetter } from '../api/client';
|
||||
import {
|
||||
SystemHealth,
|
||||
APIStats,
|
||||
@@ -32,27 +32,37 @@ const monitoringAPI = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor for authentication
|
||||
monitoringAPI.interceptors.request.use(
|
||||
(config) => {
|
||||
// Add auth token if available
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
async (config) => {
|
||||
try {
|
||||
const tokenGetter = getAuthTokenGetter();
|
||||
if (tokenGetter) {
|
||||
const token = await tokenGetter();
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
(config.headers as any).Authorization = `Bearer ${token}`;
|
||||
}
|
||||
} else {
|
||||
const legacyToken = localStorage.getItem('auth_token');
|
||||
if (legacyToken) {
|
||||
config.headers = config.headers || {};
|
||||
(config.headers as any).Authorization = `Bearer ${legacyToken}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Monitoring API: Error getting auth token', e);
|
||||
}
|
||||
|
||||
// Add user ID to ALL requests for billing tracking
|
||||
const userId = localStorage.getItem('user_id') || 'demo-user';
|
||||
|
||||
// Add user_id as query parameter for billing tracking
|
||||
if (config.params) {
|
||||
config.params.user_id = userId;
|
||||
(config.params as any).user_id = userId;
|
||||
} else {
|
||||
config.params = { user_id: userId };
|
||||
}
|
||||
|
||||
// Also add as header for additional tracking
|
||||
config.headers['X-User-ID'] = userId;
|
||||
config.headers = config.headers || {};
|
||||
(config.headers as any)['X-User-ID'] = userId;
|
||||
|
||||
return config;
|
||||
},
|
||||
@@ -61,7 +71,6 @@ monitoringAPI.interceptors.request.use(
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor for error handling
|
||||
monitoringAPI.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response;
|
||||
@@ -69,13 +78,7 @@ monitoringAPI.interceptors.response.use(
|
||||
(error) => {
|
||||
console.error('Monitoring API Error:', error);
|
||||
|
||||
// Handle specific error cases
|
||||
if (error.response?.status === 401) {
|
||||
// Unauthorized - redirect to login
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/login';
|
||||
} else if (error.response?.status === 503) {
|
||||
// Service unavailable
|
||||
if (error.response?.status === 503) {
|
||||
console.warn('Monitoring service temporarily unavailable');
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
CreateProjectResult,
|
||||
Fact,
|
||||
Knobs,
|
||||
Line,
|
||||
PodcastAnalysis,
|
||||
PodcastEstimate,
|
||||
Query,
|
||||
@@ -128,6 +127,9 @@ const mapSourcesToFacts = (sources: ExaSource[]): Fact[] => {
|
||||
url: source.url || "",
|
||||
date: source.published_at || "Unknown",
|
||||
confidence: typeof (source as any).credibility_score === "number" ? (source as any).credibility_score : Math.max(0.5, 0.85 - idx * 0.02),
|
||||
image: source.image,
|
||||
author: source.author,
|
||||
highlights: source.highlights,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -140,6 +142,8 @@ type ExaSource = {
|
||||
summary?: string;
|
||||
source_type?: string;
|
||||
index?: number;
|
||||
image?: string;
|
||||
author?: string;
|
||||
};
|
||||
|
||||
type ExaResearchResult = {
|
||||
@@ -151,15 +155,20 @@ type ExaResearchResult = {
|
||||
content?: string;
|
||||
};
|
||||
|
||||
const mapExaResearchResponse = (response: ExaResearchResult): Research => {
|
||||
const mapExaResearchResponse = (response: any): Research => {
|
||||
const factCards = mapSourcesToFacts(response.sources);
|
||||
const summary =
|
||||
response.content?.slice(0, 1200) ||
|
||||
(response.search_queries && response.search_queries.length
|
||||
? `Research completed for queries: ${response.search_queries.join(", ")}`
|
||||
: "Research completed. Review fact cards for details.");
|
||||
// Use backend summary if available, otherwise use full content (no truncation) or fallback text
|
||||
const summary = response.summary || response.content || "Research completed.";
|
||||
|
||||
const keyInsights = (response.key_insights || []).map((insight: any) => ({
|
||||
title: insight.title || "Insight",
|
||||
content: insight.content || "",
|
||||
source_indices: insight.source_indices || []
|
||||
}));
|
||||
|
||||
return {
|
||||
summary,
|
||||
keyInsights,
|
||||
factCards,
|
||||
mappedAngles: [],
|
||||
searchQueries: response.search_queries,
|
||||
@@ -170,58 +179,6 @@ const mapExaResearchResponse = (response: ExaResearchResult): Research => {
|
||||
};
|
||||
};
|
||||
|
||||
const splitIntoLines = (text: string, speakers: number): Line[] => {
|
||||
const sentences = text
|
||||
.split(/(?<=[.?!])\s+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 4);
|
||||
if (!sentences.length) {
|
||||
return [
|
||||
{
|
||||
id: createId("line"),
|
||||
speaker: "Host",
|
||||
text: text || "Let's dive into today’s topic.",
|
||||
},
|
||||
];
|
||||
}
|
||||
return sentences.map((sentence, idx) => ({
|
||||
id: createId("line"),
|
||||
speaker: idx % speakers === 0 ? "Host" : `Guest ${((idx % speakers) + 1).toString()}`,
|
||||
text: sentence,
|
||||
}));
|
||||
};
|
||||
|
||||
// Unused helper functions - kept for reference but not currently used
|
||||
// const storySceneToPodcastScene = (scene: StoryScene, knobs: Knobs, speakers: number): Scene => {
|
||||
// const text = scene.description || scene.audio_narration || scene.image_prompt || scene.title || "Narration";
|
||||
// return {
|
||||
// id: `scene-${scene.scene_number || createId("scene")}`,
|
||||
// title: scene.title || `Scene ${scene.scene_number}`,
|
||||
// duration: Math.max(20, knobs.scene_length_target || DEFAULT_KNOBS.scene_length_target),
|
||||
// lines: splitIntoLines(text, Math.max(1, speakers)),
|
||||
// approved: false,
|
||||
// };
|
||||
// };
|
||||
|
||||
// const ensureScenes = (outline: StorySetupGenerationResponse["options"] | StoryScene[] | string | undefined): StoryScene[] => {
|
||||
// if (!outline) return [];
|
||||
// if (typeof outline === "string") {
|
||||
// return [
|
||||
// {
|
||||
// scene_number: 1,
|
||||
// title: outline.slice(0, 60),
|
||||
// description: outline,
|
||||
// image_prompt: outline,
|
||||
// audio_narration: outline,
|
||||
// } as StoryScene,
|
||||
// ];
|
||||
// }
|
||||
// if (Array.isArray(outline)) {
|
||||
// return outline as StoryScene[];
|
||||
// }
|
||||
// return [];
|
||||
// };
|
||||
|
||||
const ensurePreflight = async (operation: PreflightOperation) => {
|
||||
const result = await checkPreflight(operation);
|
||||
if (!result.can_proceed) {
|
||||
@@ -232,14 +189,24 @@ const ensurePreflight = async (operation: PreflightOperation) => {
|
||||
};
|
||||
|
||||
export const podcastApi = {
|
||||
async createProject(payload: CreateProjectPayload): Promise<CreateProjectResult> {
|
||||
async createProject(payload: CreateProjectPayload, bible?: any, feedback?: string): Promise<CreateProjectResult> {
|
||||
const storyIdea = payload.ideaOrUrl || "AI marketing for small businesses";
|
||||
|
||||
await ensurePreflight({
|
||||
provider: "gemini",
|
||||
operation_type: "podcast_analysis",
|
||||
tokens_requested: 1500,
|
||||
actual_provider_name: "gemini",
|
||||
});
|
||||
|
||||
// Podcast-specific analysis (not story setup)
|
||||
const analysisResp = await aiApiClient.post("/api/podcast/analyze", {
|
||||
idea: storyIdea,
|
||||
duration: payload.duration,
|
||||
speakers: payload.speakers,
|
||||
bible: bible,
|
||||
avatar_url: payload.avatarUrl,
|
||||
feedback: feedback, // Pass feedback to backend
|
||||
});
|
||||
|
||||
const outlines = (analysisResp.data?.suggested_outlines || []).map((o: any, idx: number) => ({
|
||||
@@ -255,11 +222,24 @@ export const podcastApi = {
|
||||
suggestedOutlines: outlines,
|
||||
suggestedKnobs: { ...DEFAULT_KNOBS, ...payload.knobs },
|
||||
titleSuggestions: (analysisResp.data?.title_suggestions || []).filter(Boolean),
|
||||
research_queries: analysisResp.data?.research_queries || [],
|
||||
exaSuggestedConfig: analysisResp.data?.exa_suggested_config || undefined,
|
||||
};
|
||||
|
||||
const researchConfig = await getResearchConfig().catch(() => null);
|
||||
const queries = mapPersonaQueries(researchConfig?.research_persona, storyIdea);
|
||||
|
||||
// Use AI-generated queries if available, fallback to legacy mapping
|
||||
let queries: Query[] = [];
|
||||
if (analysis.research_queries && analysis.research_queries.length > 0) {
|
||||
queries = analysis.research_queries.map(rq => ({
|
||||
id: createId("q"),
|
||||
query: rq.query,
|
||||
rationale: rq.rationale,
|
||||
needsRecentStats: /202[45]|latest|trend/i.test(rq.query)
|
||||
}));
|
||||
} else {
|
||||
queries = mapPersonaQueries(researchConfig?.research_persona, storyIdea);
|
||||
}
|
||||
|
||||
const projectId = createId("podcast");
|
||||
const estimate = estimateCosts({
|
||||
@@ -276,15 +256,25 @@ export const podcastApi = {
|
||||
analysis,
|
||||
estimate,
|
||||
queries,
|
||||
bible: analysisResp.data?.bible || undefined,
|
||||
avatar_url: analysisResp.data?.avatar_url || null,
|
||||
avatar_prompt: analysisResp.data?.avatar_prompt || null,
|
||||
};
|
||||
},
|
||||
|
||||
async enhanceIdea(params: { idea: string; bible?: any }): Promise<{ enhanced_idea: string; rationale: string }> {
|
||||
const response = await aiApiClient.post("/api/podcast/idea/enhance", params);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async runResearch(params: {
|
||||
projectId: string;
|
||||
topic: string;
|
||||
approvedQueries: Query[];
|
||||
provider?: ResearchProvider;
|
||||
exaConfig?: ResearchConfig;
|
||||
bible?: any;
|
||||
analysis?: PodcastAnalysis | null;
|
||||
onProgress?: (message: string) => void;
|
||||
}): Promise<{ research: Research; raw: any }> {
|
||||
const keywords = params.approvedQueries.map((q) => q.query).filter(Boolean);
|
||||
@@ -317,12 +307,14 @@ export const podcastApi = {
|
||||
topic: params.topic || keywords[0],
|
||||
queries: keywords,
|
||||
exa_config: sanitizedExaConfig,
|
||||
bible: params.bible,
|
||||
analysis: params.analysis,
|
||||
});
|
||||
|
||||
const exaResult = response.data as ExaResearchResult;
|
||||
if (params.onProgress) {
|
||||
if (params.onProgress) {
|
||||
params.onProgress("Deep research completed with Exa.");
|
||||
}
|
||||
}
|
||||
const mapped = mapExaResearchResponse(exaResult);
|
||||
return { research: mapped, raw: exaResult };
|
||||
},
|
||||
@@ -334,6 +326,9 @@ export const podcastApi = {
|
||||
knobs: Knobs;
|
||||
speakers: number;
|
||||
durationMinutes: number;
|
||||
bible?: any;
|
||||
outline?: any;
|
||||
analysis?: PodcastAnalysis | null;
|
||||
}): Promise<Script> {
|
||||
await ensurePreflight({
|
||||
provider: "gemini",
|
||||
@@ -347,6 +342,9 @@ export const podcastApi = {
|
||||
duration_minutes: params.durationMinutes,
|
||||
speakers: params.speakers,
|
||||
research: params.research,
|
||||
bible: params.bible,
|
||||
outline: params.outline,
|
||||
analysis: params.analysis,
|
||||
});
|
||||
|
||||
const scenes = response.data?.scenes || [];
|
||||
@@ -564,11 +562,17 @@ export const podcastApi = {
|
||||
duration: number;
|
||||
speakers: number;
|
||||
budget_cap: number;
|
||||
avatar_url?: string | null;
|
||||
}): Promise<any> {
|
||||
const response = await aiApiClient.post("/api/podcast/projects", params);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateProject(projectId: string, updates: any): Promise<any> {
|
||||
const response = await aiApiClient.put(`/api/podcast/projects/${projectId}`, updates);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteProject(projectId: string): Promise<void> {
|
||||
await aiApiClient.delete(`/api/podcast/projects/${projectId}`);
|
||||
},
|
||||
@@ -619,6 +623,7 @@ export const podcastApi = {
|
||||
sceneTitle: string;
|
||||
audioUrl: string;
|
||||
avatarImageUrl?: string;
|
||||
bible?: any;
|
||||
resolution?: string;
|
||||
prompt?: string;
|
||||
seed?: number;
|
||||
@@ -630,6 +635,7 @@ export const podcastApi = {
|
||||
scene_title: params.sceneTitle,
|
||||
audio_url: params.audioUrl,
|
||||
avatar_image_url: params.avatarImageUrl,
|
||||
bible: params.bible,
|
||||
resolution: params.resolution || "720p",
|
||||
prompt: params.prompt,
|
||||
seed: params.seed ?? -1,
|
||||
@@ -692,6 +698,7 @@ export const podcastApi = {
|
||||
sceneTitle: string;
|
||||
sceneContent?: string;
|
||||
baseAvatarUrl?: string;
|
||||
bible?: any;
|
||||
idea?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -715,6 +722,7 @@ export const podcastApi = {
|
||||
scene_title: params.sceneTitle,
|
||||
scene_content: params.sceneContent,
|
||||
base_avatar_url: params.baseAvatarUrl || null,
|
||||
bible: params.bible,
|
||||
idea: params.idea || null,
|
||||
width: params.width || 1024,
|
||||
height: params.height || 1024,
|
||||
@@ -752,8 +760,8 @@ export const podcastApi = {
|
||||
scene_ids: params.sceneIds,
|
||||
scene_audio_urls: params.sceneAudioUrls,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async uploadAvatar(file: File, projectId?: string): Promise<{ avatar_url: string; avatar_filename: string }> {
|
||||
const formData = new FormData();
|
||||
|
||||
@@ -35,10 +35,93 @@ export interface StoryGenerationRequest {
|
||||
audio_lang?: string;
|
||||
audio_slow?: boolean;
|
||||
audio_rate?: number;
|
||||
anime_bible?: AnimeStoryBible | null;
|
||||
}
|
||||
|
||||
export interface StorySetupGenerationRequest {
|
||||
story_idea: string;
|
||||
story_mode?: 'marketing' | 'pure';
|
||||
story_template?: string | null;
|
||||
brand_context?: {
|
||||
brand_name?: string | null;
|
||||
writing_tone?: string | null;
|
||||
audience_description?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface StoryIdeaEnhanceRequest {
|
||||
story_idea: string;
|
||||
story_mode?: 'marketing' | 'pure';
|
||||
story_template?: string | null;
|
||||
brand_context?: {
|
||||
brand_name?: string | null;
|
||||
writing_tone?: string | null;
|
||||
audience_description?: string | null;
|
||||
} | null;
|
||||
fiction_variant?: string | null;
|
||||
narrative_energy?: string | null;
|
||||
}
|
||||
|
||||
export interface StoryIdeaEnhanceSuggestion {
|
||||
idea: string;
|
||||
whats_missing: string;
|
||||
why_choose: string;
|
||||
}
|
||||
|
||||
export interface StoryIdeaEnhanceResponse {
|
||||
suggestions: StoryIdeaEnhanceSuggestion[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface StoryContextResponse {
|
||||
canonical_profile: Record<string, any> | null;
|
||||
website_url?: string | null;
|
||||
research_preferences?: Record<string, any> | null;
|
||||
brand_context?: {
|
||||
brand_name?: string | null;
|
||||
writing_tone?: string | null;
|
||||
audience_description?: string | null;
|
||||
} | null;
|
||||
brand_assets?: {
|
||||
avatar_url?: string | null;
|
||||
voice_preview_url?: string | null;
|
||||
custom_voice_id?: string | null;
|
||||
} | null;
|
||||
persona_enabled?: boolean;
|
||||
has_persona_context?: boolean;
|
||||
}
|
||||
|
||||
export interface AnimeCharacter {
|
||||
id: string;
|
||||
name: string;
|
||||
age_range: string;
|
||||
role: string;
|
||||
look: string;
|
||||
outfit_palette: string;
|
||||
personality_tags: string[];
|
||||
}
|
||||
|
||||
export interface AnimeWorld {
|
||||
setting: string;
|
||||
era: string;
|
||||
tech_or_magic_level: string;
|
||||
core_rules: string[];
|
||||
}
|
||||
|
||||
export interface AnimeVisualStyle {
|
||||
style_preset: string;
|
||||
camera_style: string;
|
||||
color_mood: string;
|
||||
lighting: string;
|
||||
line_style: string;
|
||||
extra_tags: string[];
|
||||
}
|
||||
|
||||
export interface AnimeStoryBible {
|
||||
story_id?: string;
|
||||
main_cast: AnimeCharacter[];
|
||||
world: AnimeWorld;
|
||||
visual_style: AnimeVisualStyle;
|
||||
}
|
||||
|
||||
export interface StorySetupOption {
|
||||
@@ -96,6 +179,7 @@ export interface StoryOutlineResponse {
|
||||
success: boolean;
|
||||
task_id?: string;
|
||||
is_structured?: boolean;
|
||||
anime_bible?: AnimeStoryBible;
|
||||
}
|
||||
|
||||
export interface StoryContentResponse {
|
||||
@@ -261,6 +345,97 @@ export interface ResumeAnimateSceneRequest {
|
||||
duration?: 5 | 10;
|
||||
}
|
||||
|
||||
export interface AnimeSceneTextRequest {
|
||||
scene: StoryScene;
|
||||
persona: string;
|
||||
story_setting: string;
|
||||
character_input: string;
|
||||
plot_elements: string;
|
||||
writing_style: string;
|
||||
story_tone: string;
|
||||
narrative_pov: string;
|
||||
audience_age_group: string;
|
||||
content_rating: string;
|
||||
anime_bible?: AnimeStoryBible | null;
|
||||
}
|
||||
|
||||
export interface AnimeSceneTextResponse {
|
||||
scene: StoryScene;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface AnimeSceneGenerateRequest {
|
||||
premise: string;
|
||||
persona: string;
|
||||
story_setting: string;
|
||||
character_input: string;
|
||||
plot_elements: string;
|
||||
writing_style: string;
|
||||
story_tone: string;
|
||||
narrative_pov: string;
|
||||
audience_age_group: string;
|
||||
content_rating: string;
|
||||
anime_bible: AnimeStoryBible;
|
||||
previous_scenes?: StoryScene[] | null;
|
||||
target_scene_number?: number | null;
|
||||
}
|
||||
|
||||
export interface AnimeSceneGenerateResponse {
|
||||
scene: StoryScene;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface StoryProjectSummary {
|
||||
id: number;
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
title?: string | null;
|
||||
story_mode?: string | null;
|
||||
story_template?: string | null;
|
||||
setup?: Record<string, any> | null;
|
||||
outline?: Record<string, any> | null;
|
||||
scenes?: Record<string, any>[] | null;
|
||||
story_content?: Record<string, any> | null;
|
||||
anime_bible?: AnimeStoryBible | null;
|
||||
media_state?: Record<string, any> | null;
|
||||
current_phase?: string | null;
|
||||
status: string;
|
||||
is_favorite: boolean;
|
||||
is_complete: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface StoryProjectListResponse {
|
||||
projects: StoryProjectSummary[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface CreateStoryProjectRequest {
|
||||
project_id: string;
|
||||
title?: string | null;
|
||||
story_mode?: string | null;
|
||||
story_template?: string | null;
|
||||
setup?: Record<string, any> | null;
|
||||
}
|
||||
|
||||
export interface UpdateStoryProjectRequest {
|
||||
title?: string | null;
|
||||
story_mode?: string | null;
|
||||
story_template?: string | null;
|
||||
setup?: Record<string, any> | null;
|
||||
outline?: Record<string, any> | null;
|
||||
scenes?: Record<string, any>[] | null;
|
||||
story_content?: Record<string, any> | null;
|
||||
anime_bible?: AnimeStoryBible | null;
|
||||
media_state?: Record<string, any> | null;
|
||||
current_phase?: string | null;
|
||||
status?: string | null;
|
||||
is_complete?: boolean | null;
|
||||
}
|
||||
|
||||
class StoryWriterApi {
|
||||
/**
|
||||
* Generate 3 story setup options from a user's story idea
|
||||
@@ -275,6 +450,24 @@ class StoryWriterApi {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async enhanceStoryIdea(
|
||||
request: StoryIdeaEnhanceRequest
|
||||
): Promise<StoryIdeaEnhanceResponse> {
|
||||
const response = await aiApiClient.post<StoryIdeaEnhanceResponse>(
|
||||
"/api/story/enhance-idea",
|
||||
request
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get onboarding-based story context for Story Studio
|
||||
*/
|
||||
async getStoryContext(): Promise<StoryContextResponse> {
|
||||
const response = await aiApiClient.get<StoryContextResponse>("/api/story/context");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a story premise
|
||||
*/
|
||||
@@ -441,6 +634,71 @@ class StoryWriterApi {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async refineAnimeSceneText(request: AnimeSceneTextRequest): Promise<AnimeSceneTextResponse> {
|
||||
const response = await aiApiClient.post<AnimeSceneTextResponse>(
|
||||
"/api/story/anime/scene-text",
|
||||
request
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async generateAnimeSceneFromBible(
|
||||
request: AnimeSceneGenerateRequest
|
||||
): Promise<AnimeSceneGenerateResponse> {
|
||||
const response = await aiApiClient.post<AnimeSceneGenerateResponse>(
|
||||
"/api/story/anime/scene-generate",
|
||||
request
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createStoryProject(
|
||||
payload: CreateStoryProjectRequest
|
||||
): Promise<StoryProjectSummary> {
|
||||
const response = await aiApiClient.post<StoryProjectSummary>("/api/story/projects", payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async loadStoryProject(projectId: string): Promise<StoryProjectSummary> {
|
||||
const response = await aiApiClient.get<StoryProjectSummary>(`/api/story/projects/${projectId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateStoryProject(
|
||||
projectId: string,
|
||||
payload: UpdateStoryProjectRequest
|
||||
): Promise<StoryProjectSummary> {
|
||||
const response = await aiApiClient.put<StoryProjectSummary>(
|
||||
`/api/story/projects/${projectId}`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async listStoryProjects(params?: {
|
||||
status?: string;
|
||||
favorites_only?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order_by?: "updated_at" | "created_at";
|
||||
}): Promise<StoryProjectListResponse> {
|
||||
const response = await aiApiClient.get<StoryProjectListResponse>("/api/story/projects", {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteStoryProject(projectId: string): Promise<void> {
|
||||
await aiApiClient.delete(`/api/story/projects/${projectId}`);
|
||||
}
|
||||
|
||||
async toggleStoryProjectFavorite(projectId: string): Promise<StoryProjectSummary> {
|
||||
const response = await aiApiClient.post<StoryProjectSummary>(
|
||||
`/api/story/projects/${projectId}/favorite`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private buildAbsoluteUrl(path: string): string {
|
||||
if (!path) return path;
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
|
||||
Reference in New Issue
Block a user