Add AI-assisted world and character drafting flows
All checks were successful
CI / validate (push) Successful in 14m3s

This commit is contained in:
2026-08-17 09:25:37 +05:00
parent 830aa5dc1f
commit cdeea8653c
23 changed files with 811 additions and 92 deletions

View File

@@ -1,12 +1,23 @@
<script setup lang="ts">
import type { WorldStarter } from '@dng/shared'
import { WorldStarterSchema, type WorldStarter } from '@dng/shared'
type Stage = 'seed' | 'questions' | 'generating' | 'preview' | 'confirming'
interface Message { id: string; role: 'coauthor' | 'player' | 'status'; body: string; label?: string }
interface DynamicQuestion { id: string; label: string; options: string[] }
interface RespondResult { readyToGenerate: boolean; question?: DynamicQuestion }
interface StoredMessage { role: 'user' | 'assistant'; content: string }
interface StoredSession {
id: string
status: 'collecting' | 'generating' | 'ready' | 'failed' | 'confirmed'
messages: StoredMessage[]
current_question?: DynamicQuestion | null
generatedWorld?: unknown
generated_world?: unknown
confirmed_world_id?: string | null
}
const { api } = useDngApi()
const route = useRoute()
const stage = ref<Stage>('seed')
const busy = ref(false)
const sessionId = ref<string | null>(null)
@@ -47,6 +58,63 @@ async function begin() {
}
}
async function resume(id: string) {
busy.value = true
retryAction.value = 'respond'
try {
const result = await api<{ session: StoredSession }>(`/api/v1/coauthor/sessions/${id}`)
const session = result.session
sessionId.value = session.id
const storedMessages = Array.isArray(session.messages) ? session.messages : []
seed.value = storedMessages.find(message => message.role === 'user')?.content ?? seed.value
answerCount.value = Math.max(0, storedMessages.filter(message => message.role === 'user').length - 1)
let questionNumber = 0
messages.value = [messages.value[0]!, ...storedMessages.map((message, index) => {
if (message.role === 'assistant') questionNumber += 1
return {
id: `stored-${index}`,
role: message.role === 'assistant' ? 'coauthor' as const : 'player' as const,
body: message.content,
label: message.role === 'assistant' ? `COAUTHOR · QUESTION ${questionNumber} OF 4` : undefined,
}
})]
if (session.status === 'confirmed') {
await navigateTo('/dashboard')
return
}
if (session.status === 'ready') {
const parsed = WorldStarterSchema.safeParse(session.generatedWorld ?? session.generated_world)
if (!parsed.success) throw new Error('This saved world draft is invalid. Generate it again.')
draft.value = parsed.data
stage.value = 'preview'
addMessage('status', `${parsed.data.title}” was restored.`, 'DRAFT RESTORED')
return
}
if (session.status === 'generating') {
busy.value = false
await generate()
return
}
if (session.status === 'failed') {
stage.value = 'questions'
retryAction.value = 'generate'
addMessage('status', 'The previous generation failed. Your answers are safe.', 'ERROR')
return
}
stage.value = 'questions'
currentQuestion.value = session.current_question ?? null
if (!currentQuestion.value) await requestNextQuestion()
} catch (error) {
stage.value = 'seed'
sessionId.value = null
addMessage('status', errorText(error) || 'This saved coauthor session could not be restored.', 'ERROR')
} finally {
busy.value = false
}
}
async function answerQuestion(key: string, value: string) {
if (stage.value !== 'questions' || currentQuestion.value?.id !== key || busy.value || !sessionId.value) return
busy.value = true
@@ -154,6 +222,11 @@ async function confirm() {
stage.value = 'preview'
}
}
onMounted(() => {
const resumeId = typeof route.query.session === 'string' ? route.query.session : ''
if (resumeId) void resume(resumeId)
})
</script>
<template>
@@ -164,7 +237,7 @@ async function confirm() {
<div class="progress"><i :style="{ width: `${progress}%` }" /></div>
<ol>
<li :class="{ active: stage === 'seed', done: stage !== 'seed' }"><b>01</b><span>Seed idea<small>Say what cannot exist yet.</small></span></li>
<li :class="{ active: stage === 'questions', done: answerCount >= 3 }"><b>02</b><span>Shape the signal<small>{{ answerCount }} / up to 5 answers captured.</small></span></li>
<li :class="{ active: stage === 'questions', done: answerCount >= 4 }"><b>02</b><span>Shape the signal<small>{{ answerCount }} / 4 answers captured.</small></span></li>
<li :class="{ active: stage === 'generating', done: stage === 'preview' || stage === 'confirming' }"><b>03</b><span>Generate structure<small>A playable starting kit.</small></span></li>
<li :class="{ active: stage === 'preview' || stage === 'confirming' }"><b>04</b><span>Review & launch<small>You remain the final author.</small></span></li>
</ol>