feat: add home page and refactor workspace entry UI
This commit is contained in:
156
src/lib/home/create-project-launch.ts
Normal file
156
src/lib/home/create-project-launch.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user