Add AI-assisted world and character drafting flows
All checks were successful
CI / validate (push) Successful in 14m3s
All checks were successful
CI / validate (push) Successful in 14m3s
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user