Files
waooplus/src/lib/home/create-project-launch.ts

157 lines
4.1 KiB
TypeScript

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<Response>
}
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<string, unknown> | null {
if (!value || typeof value !== 'object') return null
return value as Record<string, unknown>
}
function readNestedString(
source: Record<string, unknown> | 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<string> {
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<string> {
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<string> {
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<CreateHomeProjectLaunchResult> {
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),
}
}