53 lines
3.4 KiB
Vue
53 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
interface Message { id: string; role: 'coauthor' | 'player' | 'status'; body: string; label?: string }
|
|
interface Question { key: string; label: string; options: string[] }
|
|
|
|
const props = defineProps<{
|
|
messages: Message[]
|
|
question: Question | null
|
|
selectedAnswer?: string
|
|
busy?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
answer: [key: string, value: string]
|
|
retry: []
|
|
}>()
|
|
|
|
const customAnswer = ref('')
|
|
|
|
function submitCustom() {
|
|
if (!props.question || !customAnswer.value.trim()) return
|
|
emit('answer', props.question.key, customAnswer.value.trim())
|
|
customAnswer.value = ''
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="conversation" aria-live="polite">
|
|
<ol class="messages">
|
|
<li v-for="message in messages" :key="message.id" :class="message.role">
|
|
<small>{{ message.label || (message.role === 'player' ? 'YOU' : message.role === 'status' ? 'SYSTEM' : 'COAUTHOR') }}</small>
|
|
<p>{{ message.body }}</p>
|
|
</li>
|
|
</ol>
|
|
|
|
<div v-if="question" class="question-card">
|
|
<p>{{ question.label }}</p>
|
|
<div class="options">
|
|
<button v-for="option in question.options" :key="option" type="button" :class="{ selected: selectedAnswer === option }" :disabled="busy" @click="emit('answer', question.key, option)">{{ option }}</button>
|
|
</div>
|
|
<form class="custom" @submit.prevent="submitCustom">
|
|
<input v-model="customAnswer" :disabled="busy" maxlength="160" placeholder="Or write your own answer…" :aria-label="`Custom answer: ${question.label}`">
|
|
<button :disabled="busy || !customAnswer.trim()">SEND →</button>
|
|
</form>
|
|
</div>
|
|
|
|
<button v-if="messages.at(-1)?.role === 'status' && messages.at(-1)?.label === 'ERROR'" class="retry" type="button" @click="emit('retry')">TRY AGAIN</button>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.conversation{max-width:780px}.messages{list-style:none;padding:0;margin:0}.messages li{max-width:680px;margin:22px 0;padding:5px 20px;border-left:2px solid var(--acid)}.messages li.player{margin-left:auto;border-left:0;border-right:2px solid #5f5f58;text-align:right}.messages li.status{border:1px solid var(--line);background:#10100f}.messages li.status[aria-invalid=true]{border-color:#7b4137}.messages small{font:500 8px var(--mono);letter-spacing:.15em;color:var(--acid)}.messages .player small{color:var(--ink)}.messages .status small{color:var(--muted)}.messages p{margin:9px 0 0;font-size:14px;line-height:1.65;color:#d0d0ca;white-space:pre-wrap}.question-card{margin:28px 0 0 20px;padding:24px;border:1px solid var(--line);background:#0d0d0c}.question-card>p{margin:0 0 18px;font:600 14px/1.5 var(--display)}.options{display:flex;gap:9px;flex-wrap:wrap}.options button,.retry{padding:12px 14px;border:1px solid var(--line);background:#10100f;color:var(--muted);font:500 9px var(--mono)}.options button:hover,.options button.selected{border-color:var(--acid);color:var(--acid)}button:disabled{opacity:.45;cursor:not-allowed}.custom{display:flex;margin-top:12px}.custom input{min-width:0;flex:1;padding:13px 14px;border:1px solid var(--line);background:#080808;color:var(--ink);font:12px var(--body)}.custom button{border:0;background:var(--acid);color:#080808;padding:0 17px;font:600 8px var(--mono)}.retry{margin:18px 0 0 20px;color:#ff9d85;border-color:#7b4137}@media(max-width:600px){.messages li.player{margin-left:28px}.question-card{margin-left:0}.custom{flex-direction:column}.custom button{min-height:42px}}
|
|
</style>
|