interface ApiErrorPayload { error?: string | { message?: string } | null } interface ProjectCreationPayload { project?: { id?: string | null } | null } interface EpisodeCreationPayload { episode?: { id?: string | null } | null } interface ApiFetchLike { (input: string, init?: RequestInit): Promise } export interface HomeWorkspaceLaunchTarget { pathname: string query: { episode: string autoRun?: 'storyToScript' } } export interface CreateHomeProjectLaunchParams { apiFetch: ApiFetchLike projectName: string storyText: string videoRatio: string artStyle: string episodeName: string } export interface CreateHomeProjectLaunchResult { projectId: string episodeId: string target: HomeWorkspaceLaunchTarget } function readObject(value: unknown): Record | null { if (!value || typeof value !== 'object') return null return value as Record } function readNestedString( source: Record | null, outerKey: string, innerKey: string, ): string | null { const outer = readObject(source?.[outerKey]) const value = outer?.[innerKey] return typeof value === 'string' && value.trim() ? value : null } async function readApiErrorMessage(response: Response, fallback: string): Promise { try { const payload = await response.json() as ApiErrorPayload if (typeof payload?.error === 'string' && payload.error.trim()) { return payload.error } if (payload?.error && typeof payload.error === 'object' && typeof payload.error.message === 'string' && payload.error.message.trim()) { return payload.error.message } } catch { // Keep the explicit fallback when the backend does not return JSON. } return fallback } async function readProjectId(response: Response): Promise { const payload = await response.json() as ProjectCreationPayload const projectId = readNestedString(readObject(payload), 'project', 'id') if (!projectId) { throw new Error('Project creation response missing project id') } return projectId } async function readEpisodeId(response: Response): Promise { const payload = await response.json() as EpisodeCreationPayload const episodeId = readNestedString(readObject(payload), 'episode', 'id') if (!episodeId) { throw new Error('Episode creation response missing episode id') } return episodeId } export function buildHomeWorkspaceLaunchTarget(projectId: string, episodeId: string): HomeWorkspaceLaunchTarget { return { pathname: `/workspace/${projectId}`, query: { episode: episodeId, autoRun: 'storyToScript', }, } } export async function createHomeProjectLaunch({ apiFetch, projectName, storyText, videoRatio, artStyle, episodeName, }: CreateHomeProjectLaunchParams): Promise { const projectResponse = await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: projectName, description: storyText, mode: 'novel-promotion', }), }) if (!projectResponse.ok) { throw new Error(await readApiErrorMessage(projectResponse, 'Failed to create project')) } const projectId = await readProjectId(projectResponse) const configResponse = await apiFetch(`/api/novel-promotion/${projectId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ videoRatio, artStyle }), }) if (!configResponse.ok) { throw new Error(await readApiErrorMessage(configResponse, 'Failed to save project config')) } const episodeResponse = await apiFetch(`/api/novel-promotion/${projectId}/episodes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: episodeName, novelText: storyText, }), }) if (!episodeResponse.ok) { throw new Error(await readApiErrorMessage(episodeResponse, 'Failed to create first episode')) } const episodeId = await readEpisodeId(episodeResponse) return { projectId, episodeId, target: buildHomeWorkspaceLaunchTarget(projectId, episodeId), } }