This commit is contained in:
Dmitry 2026-01-10 21:01:01 +03:00
parent dbc43d65c1
commit eb13424f20
20 changed files with 409 additions and 66 deletions

View file

@ -211,7 +211,7 @@ export const packsApi = {
getTemplate: async (): Promise<{
templateVersion: string
instructions: string
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
questions: Array<{ text: string; answers: Array<{ text: string; points: number }> }>
}> => {
try {
const response = await adminApiClient.get('/api/admin/packs/export/template')
@ -239,7 +239,7 @@ export const packsApi = {
category: string
isPublic: boolean
}
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
questions: Array<{ text: string; answers: Array<{ text: string; points: number }> }>
}> => {
try {
const response = await adminApiClient.get(`/api/admin/packs/${packId}/export`)
@ -273,7 +273,7 @@ export const packsApi = {
description: string
category: string
isPublic: boolean
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
questions: Array<{ text: string; answers: Array<{ text: string; points: number }> }>
}): Promise<EditCardPackDto> => {
try {
const response = await adminApiClient.post('/api/admin/packs/import', data)

View file

@ -42,7 +42,7 @@ export function GameQuestionEditorDialog({
useEffect(() => {
if (open) {
if (question) {
setQuestionText(question.question || '')
setQuestionText(question.text || '')
setAnswers(
question.answers && question.answers.length > 0
? [...question.answers]
@ -108,7 +108,7 @@ export function GameQuestionEditorDialog({
}
const questionData: Question = {
question: questionText.trim(),
text: questionText.trim(),
answers: validAnswers,
}

View file

@ -82,7 +82,7 @@ export function GameQuestionsManager({
</span>
</div>
<div>
<p className="font-medium">{question.question}</p>
<p className="font-medium">{question.text}</p>
<p className="text-sm text-muted-foreground">
{question.answers.length} answer
{question.answers.length !== 1 ? 's' : ''} (points:{' '}

View file

@ -78,7 +78,8 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
// Validate questions structure
const isValid = questions.every(
(q: any) =>
(q.question || q.text) &&
q.text &&
typeof q.text === 'string' &&
Array.isArray(q.answers) &&
q.answers.length > 0 &&
q.answers.every(
@ -87,7 +88,7 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
)
if (!isValid) {
throw new Error('Invalid question format. Each question must have "question" (or "text") and "answers" array with "text" and "points" fields.')
throw new Error('Invalid question format. Each question must have "text" field and "answers" array with "text" and "points" fields.')
}
setParseError(null)
@ -151,14 +152,18 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
return
}
// Normalize questions to use 'question' field
const normalizedQuestions = parsed.map((q: any) => ({
question: q.question || q.text,
answers: q.answers.map((a: any) => ({
text: a.text,
points: a.points,
})),
}))
// Normalize questions to use 'text' field (ignore 'question' field if present)
const normalizedQuestions = parsed.map((q: any) => {
const { question, ...rest } = q
return {
...rest,
text: q.text || '',
answers: q.answers.map((a: any) => ({
text: a.text,
points: a.points,
})),
}
})
setIsLoading(true)
try {
@ -227,7 +232,7 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
id="jsonContent"
value={jsonContent}
onChange={(e) => handleJsonChange(e.target.value)}
placeholder='[{"question": "...", "answers": [{"text": "...", "points": 100}]}]'
placeholder='[{"text": "...", "answers": [{"text": "...", "points": 100}]}]'
rows={8}
className="font-mono text-sm"
/>

View file

@ -55,7 +55,7 @@ export function InputButtonsQuestionForm({
const addButton = () => {
const newButton: TestButton = {
id: `btn_${Date.now()}`,
id: crypto.randomUUID(),
text: '',
}
setButtons([...buttons, newButton])

View file

@ -53,7 +53,7 @@ export function SimpleQuestionForm({
const addButton = () => {
const newButton: TestButton = {
id: `btn_${Date.now()}`,
id: crypto.randomUUID(),
text: '',
}
setButtons([...buttons, newButton])

View file

@ -158,10 +158,10 @@ export default function PacksPage() {
const fullPack = await packsApi.getPack(pack.id)
setSelectedPack(fullPack)
// Convert questions from backend format (supports both 'question' and 'text' fields)
// Convert questions from backend format (using 'text' field)
const questions: Question[] = Array.isArray(fullPack.questions)
? fullPack.questions.map((q: any) => ({
question: q.question || q.text || '',
text: q.text || '',
answers: Array.isArray(q.answers)
? q.answers.map((a: any) => ({
text: a.text || '',

View file

@ -7,7 +7,7 @@ export interface Answer {
}
export interface Question {
question: string
text: string
answers: Answer[]
}

View file

@ -16,25 +16,26 @@ interface Answer {
interface Question {
id?: string;
text?: string;
question?: string;
answers: Answer[];
}
function ensureQuestionIds(questions: Question[]): Question[] {
return questions.map((question) => {
const questionId = question.id || randomUUID();
const questionText = question.text || question.question || '';
const questionText = question.text || '';
const answersWithIds = question.answers.map((answer) => ({
...answer,
id: answer.id || randomUUID(),
}));
// Удаляем поле question если оно было в исходном объекте
const { question: _, ...questionWithoutQuestion } = question;
return {
...question,
...questionWithoutQuestion,
id: questionId,
text: questionText,
question: questionText, // Keep both fields for compatibility
answers: answersWithIds,
};
});

View file

@ -35,10 +35,10 @@ export class AdminPacksController {
return {
templateVersion: '1.0',
instructions:
'Fill in your questions below. Each question must have a "question" field and an "answers" array with "text" and "points" fields.',
'Fill in your questions below. Each question must have a "text" field and an "answers" array with "text" and "points" fields.',
questions: [
{
question: 'Your question here',
text: 'Your question here',
answers: [
{ text: 'Answer 1', points: 100 },
{ text: 'Answer 2', points: 80 },
@ -56,8 +56,8 @@ export class AdminPacksController {
// Validate question structure
const isValid = importPackDto.questions.every(
(q) =>
q.question &&
typeof q.question === 'string' &&
q.text &&
typeof q.text === 'string' &&
Array.isArray(q.answers) &&
q.answers.length > 0 &&
q.answers.every(
@ -70,7 +70,7 @@ export class AdminPacksController {
if (!isValid) {
throw new BadRequestException(
'Invalid question format. Each question must have a "question" field and an "answers" array with "text" and "points" fields.',
'Invalid question format. Each question must have a "text" field and an "answers" array with "text" and "points" fields.',
);
}

View file

@ -170,6 +170,10 @@ export class AdminPacksService {
throw new NotFoundException('Question pack not found');
}
// Нормализуем вопросы при экспорте, удаляя поле question если оно есть
const packQuestions = Array.isArray(pack.questions) ? pack.questions as any[] : [];
const normalizedQuestions = ensureQuestionIds(packQuestions);
return {
templateVersion: '1.0',
exportedAt: new Date().toISOString(),
@ -179,7 +183,7 @@ export class AdminPacksService {
category: pack.category,
isPublic: pack.isPublic,
},
questions: pack.questions,
questions: normalizedQuestions,
};
}
}

View file

@ -19,7 +19,7 @@ class QuestionDto {
id?: string;
@IsString()
question: string;
text: string;
@IsArray()
@ValidateNested({ each: true })

View file

@ -18,7 +18,7 @@ class ImportAnswerDto {
class ImportQuestionDto {
@IsString()
question: string;
text: string;
@IsArray()
@ValidateNested({ each: true })

View file

@ -19,7 +19,7 @@ class QuestionDto {
id?: string;
@IsString()
question: string;
text: string;
@IsArray()
@ValidateNested({ each: true })

View file

@ -27,7 +27,6 @@ interface PlayerAction {
interface Question {
id: string;
text?: string;
question?: string;
answers: Array<{
id: string;
text: string;
@ -441,7 +440,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
}
return {
id: questionId || `temp-${Math.random()}`,
text: q.text || q.question || '',
text: q.text || '',
answers: (q.answers || []).map((a: any) => ({
id: a.id || `answer-${Math.random()}`,
text: a.text || '',

View file

@ -9,7 +9,6 @@ interface Answer {
interface Question {
id?: string;
text?: string;
question?: string; // Поддержка обоих вариантов названия поля
answers: Answer[];
}
@ -34,7 +33,7 @@ export function ensureQuestionIds(questions: Question[]): Question[] {
return questions.map((question) => {
// Если ID нет или не является валидным UUID, создаем новый
const questionId = (question.id && isValidUUID(question.id)) ? question.id : randomUUID();
const questionText = question.text || question.question || '';
const questionText = question.text || '';
const answersWithIds = question.answers.map((answer) => {
// Если ID нет или не является валидным UUID, создаем новый
@ -45,11 +44,13 @@ export function ensureQuestionIds(questions: Question[]): Question[] {
};
});
// Удаляем поле question если оно было в исходном объекте
const { question: _, ...questionWithoutQuestion } = question;
return {
...question,
...questionWithoutQuestion,
id: questionId,
text: questionText,
question: questionText, // Сохраняем оба поля для совместимости
answers: answersWithIds,
};
});

View file

@ -11,7 +11,6 @@ interface Answer {
interface Question {
id: string;
text?: string;
question?: string;
answers?: Answer[];
}
@ -65,7 +64,7 @@ export class VoiceService {
}
if (contentType === TTSContentType.QUESTION) {
const questionText = question.text || question.question;
const questionText = question.text;
if (!questionText) {
this.logger.error(`Question text is empty for questionId=${questionId}`);
throw new NotFoundException('Question text is empty');

View file

@ -824,10 +824,102 @@
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2));
border-radius: var(--border-radius-sm, 8px);
transition: all 0.2s;
cursor: grab;
position: relative;
}
.questions-tab-content .questions-modal-item:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--accent-primary, #ffd700);
}
.questions-tab-content .questions-modal-item.dragging {
opacity: 0.5;
cursor: grabbing !important;
}
.questions-tab-content .questions-modal-item.drag-over {
background: rgba(255, 215, 0, 0.1);
transform: translateY(-2px);
}
.questions-tab-content .questions-modal-item.drag-over-above {
border-top: 3px solid var(--accent-primary, #ffd700);
padding-top: calc(1rem - 1px);
}
.questions-tab-content .questions-modal-item.drag-over-below {
border-bottom: 3px solid var(--accent-primary, #ffd700);
padding-bottom: calc(1rem - 1px);
}
.questions-tab-content .questions-modal-item button {
cursor: pointer;
}
.questions-tab-content .questions-modal-item button:hover {
cursor: pointer !important;
}
.questions-tab-content .questions-modal-item-order {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 50px;
}
.questions-tab-content .questions-modal-item-number {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: var(--accent-primary, #ffd700);
color: var(--bg-primary, #000000);
border-radius: 50%;
font-weight: bold;
font-size: 0.9rem;
}
.questions-tab-content .questions-modal-item-order-buttons {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.questions-tab-content .questions-modal-order-button {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2));
border-radius: 4px;
color: var(--text-primary, #ffffff);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
transition: all 0.2s;
padding: 0;
}
.questions-tab-content .questions-modal-order-button:hover:not(:disabled) {
background: var(--accent-primary, #ffd700);
color: var(--bg-primary, #000000);
border-color: var(--accent-primary, #ffd700);
transform: scale(1.1);
}
.questions-tab-content .questions-modal-order-button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.questions-tab-content .questions-modal-item-content {
@ -850,6 +942,33 @@
gap: 0.5rem;
}
.questions-tab-content .questions-modal-item-drag-handle {
color: var(--text-secondary, rgba(255, 255, 255, 0.4));
font-size: 1rem;
line-height: 1;
cursor: grab;
padding: 0.5rem 0.25rem;
user-select: none;
opacity: 0;
transition: opacity 0.2s, color 0.2s, transform 0.2s;
display: flex;
align-items: center;
justify-content: center;
min-width: 24px;
letter-spacing: -0.3rem;
}
.questions-tab-content .questions-modal-item:hover .questions-modal-item-drag-handle {
opacity: 1;
}
.questions-tab-content .questions-modal-item.dragging .questions-modal-item-drag-handle,
.questions-tab-content .questions-modal-item:hover .questions-modal-item-drag-handle:hover {
color: var(--accent-primary, #ffd700);
cursor: grabbing;
transform: scale(1.2);
}
.questions-tab-content .questions-modal-edit-button,
.questions-tab-content .questions-modal-delete-button {
width: 32px;
@ -902,6 +1021,20 @@
.questions-tab-content .questions-modal-actions button {
width: 100%;
}
.questions-tab-content .questions-modal-item {
flex-wrap: wrap;
}
.questions-tab-content .questions-modal-item-order {
min-width: 40px;
}
.questions-tab-content .questions-modal-order-button {
width: 24px;
height: 24px;
font-size: 0.8rem;
}
}
/* Custom Scrollbar */

View file

@ -83,6 +83,11 @@ const GameManagementModal = ({
const [viewingQuestion, setViewingQuestion] = useState(null)
const [showAnswers, setShowAnswers] = useState(false)
// Drag and drop state
const [draggedQuestionIndex, setDraggedQuestionIndex] = useState(null)
const [dragOverIndex, setDragOverIndex] = useState(null)
const [dragOverPosition, setDragOverPosition] = useState(null) // 'above' | 'below'
// Сбрасываем вкладку на initialTab при открытии модального окна
useEffect(() => {
if (isOpen) {
@ -237,11 +242,12 @@ const GameManagementModal = ({
if (!validateQuestionForm()) return
const questionData = {
id: editingQuestion ? editingQuestion.id : Date.now(),
id: editingQuestion ? editingQuestion.id : crypto.randomUUID(),
text: questionText.trim(),
answers: answers
.filter(a => a.text.trim())
.map(a => ({
id: a.id || crypto.randomUUID(),
text: a.text.trim(),
points: a.points,
})),
@ -270,9 +276,139 @@ const GameManagementModal = ({
}
}
const handleMoveQuestionUp = (index) => {
if (index === 0) return
const updatedQuestions = [...questions]
const temp = updatedQuestions[index]
updatedQuestions[index] = updatedQuestions[index - 1]
updatedQuestions[index - 1] = temp
onUpdateQuestions(updatedQuestions)
}
const handleMoveQuestionDown = (index) => {
if (index === questions.length - 1) return
const updatedQuestions = [...questions]
const temp = updatedQuestions[index]
updatedQuestions[index] = updatedQuestions[index + 1]
updatedQuestions[index + 1] = temp
onUpdateQuestions(updatedQuestions)
}
// Drag and drop handlers
const handleDragStart = (e, index) => {
// Предотвращаем перетаскивание при клике на кнопки или инпуты
const target = e.target
const clickedButton = target.tagName === 'BUTTON' || target.closest('button')
const clickedInput = target.tagName === 'INPUT' || target.closest('input')
// Если кликнули на кнопку или инпут, не запускаем drag
if (clickedButton || clickedInput) {
e.preventDefault()
return false
}
setDraggedQuestionIndex(index)
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', index.toString())
// Находим элемент вопроса и делаем его полупрозрачным
const item = e.currentTarget
item.style.opacity = '0.5'
}
const handleDragEnd = (e) => {
// Восстанавливаем opacity элемента
const item = e.currentTarget
item.style.opacity = ''
setDraggedQuestionIndex(null)
setDragOverIndex(null)
setDragOverPosition(null)
}
const handleDragOver = (e, index) => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
if (draggedQuestionIndex === null || draggedQuestionIndex === index) {
setDragOverIndex(null)
setDragOverPosition(null)
return
}
// Определяем, куда вставлять элемент (выше или ниже)
const rect = e.currentTarget.getBoundingClientRect()
const mouseY = e.clientY
const elementMiddleY = rect.top + rect.height / 2
const position = mouseY < elementMiddleY ? 'above' : 'below'
if (dragOverIndex !== index || dragOverPosition !== position) {
setDragOverIndex(index)
setDragOverPosition(position)
}
}
const handleDragLeave = () => {
setDragOverIndex(null)
setDragOverPosition(null)
}
const handleDrop = (e, dropIndex) => {
e.preventDefault()
e.stopPropagation()
if (draggedQuestionIndex === null || draggedQuestionIndex === dropIndex) {
setDragOverIndex(null)
setDragOverPosition(null)
setDraggedQuestionIndex(null)
return
}
const updatedQuestions = [...questions]
const draggedQuestion = updatedQuestions[draggedQuestionIndex]
// Определяем финальную позицию вставки на основе позиции перетаскивания
let insertIndex = dropIndex
if (dragOverPosition === 'below') {
insertIndex = dropIndex + 1
}
// Удаляем элемент из старой позиции
updatedQuestions.splice(draggedQuestionIndex, 1)
// Корректируем индекс вставки после удаления элемента
// Если удалили элемент выше позиции вставки, нужно уменьшить индекс на 1
if (draggedQuestionIndex < insertIndex) {
insertIndex -= 1
}
// Ограничиваем индекс диапазоном массива
insertIndex = Math.max(0, Math.min(insertIndex, updatedQuestions.length))
// Вставляем элемент в новую позицию
updatedQuestions.splice(insertIndex, 0, draggedQuestion)
onUpdateQuestions(updatedQuestions)
setDraggedQuestionIndex(null)
setDragOverIndex(null)
setDragOverPosition(null)
}
const handleExportJson = () => {
try {
const jsonString = JSON.stringify(questions, null, 2)
// Нормализуем вопросы, удаляя поле question если оно есть
const normalizedQuestions = questions.map(q => {
const { question, ...questionWithoutQuestion } = q
return {
...questionWithoutQuestion,
text: q.text || '',
answers: q.answers.map(a => ({
id: a.id,
text: a.text,
points: a.points
}))
}
})
const jsonString = JSON.stringify(normalizedQuestions, null, 2)
const blob = new Blob([jsonString], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
@ -361,15 +497,19 @@ const GameManagementModal = ({
return
}
// Добавляем id если его нет
const questionsWithIds = jsonContent.map((q, idx) => ({
...q,
id: q.id || Date.now() + Math.random() + idx,
answers: q.answers.map((a, aidx) => ({
...a,
id: a.id || `answer-${Date.now()}-${idx}-${aidx}`
}))
}))
// Нормализуем вопросы, удаляя поле question если оно есть, и добавляем id если его нет
const questionsWithIds = jsonContent.map((q) => {
const { question, ...rest } = q
return {
...rest,
id: q.id || crypto.randomUUID(),
text: q.text || '',
answers: q.answers.map((a) => ({
...a,
id: a.id || crypto.randomUUID()
}))
}
})
onUpdateQuestions(questionsWithIds)
setJsonError('')
@ -410,7 +550,7 @@ const GameManagementModal = ({
// Фильтрация вопросов по поисковому запросу
const filteredPackQuestions = packQuestions.filter((q) => {
if (!searchQuery.trim()) return true
const questionText = (q.text || q.question || '').toLowerCase()
const questionText = (q.text || '').toLowerCase()
return questionText.includes(searchQuery.toLowerCase())
})
@ -476,10 +616,14 @@ const GameManagementModal = ({
const indices = Array.from(selectedQuestionIndices)
const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean)
const copiedQuestions = questionsToImport.map((q, idx) => ({
id: Date.now() + Math.random() + idx, // Generate new ID
text: q.text || q.question || '',
answers: (q.answers || []).map(a => ({ text: a.text, points: a.points })),
const copiedQuestions = questionsToImport.map((q) => ({
id: crypto.randomUUID(),
text: q.text || '',
answers: (q.answers || []).map(a => ({
id: a.id || crypto.randomUUID(),
text: a.text,
points: a.points
})),
}))
const updatedQuestions = [...questions, ...copiedQuestions]
@ -1061,7 +1205,7 @@ const GameManagementModal = ({
onChange={() => handleToggleQuestion(originalIndex)}
/>
<div className="pack-question-content">
<strong>{q.text || q.question}</strong>
<strong>{q.text || ''}</strong>
<span className="pack-question-info">
{q.answers?.length || 0} ответов
</span>
@ -1096,7 +1240,7 @@ const GameManagementModal = ({
</div>
<div className="pack-question-viewer-content">
<div className="pack-question-viewer-text">
{viewingQuestion.text || viewingQuestion.question}
{viewingQuestion.text || ''}
</div>
<button
className="pack-show-answers-button"
@ -1197,8 +1341,50 @@ const GameManagementModal = ({
<p className="questions-modal-empty">Нет вопросов. Добавьте вопросы для игры.</p>
) : (
<div className="questions-modal-items">
{questions.map((question) => (
<div key={question.id} className="questions-modal-item">
{questions.map((question, index) => (
<div
key={question.id}
className={`questions-modal-item ${
draggedQuestionIndex === index ? 'dragging' : ''
} ${dragOverIndex === index ? 'drag-over' : ''} ${
dragOverIndex === index && dragOverPosition ? `drag-over-${dragOverPosition}` : ''
}`}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, index)}
>
<div className="questions-modal-item-order">
<span className="questions-modal-item-number">{index + 1}</span>
<div className="questions-modal-item-order-buttons">
<button
className="questions-modal-order-button"
onClick={(e) => {
e.stopPropagation()
handleMoveQuestionUp(index)
}}
onMouseDown={(e) => e.stopPropagation()}
disabled={index === 0}
title="Переместить вверх"
>
</button>
<button
className="questions-modal-order-button"
onClick={(e) => {
e.stopPropagation()
handleMoveQuestionDown(index)
}}
onMouseDown={(e) => e.stopPropagation()}
disabled={index === questions.length - 1}
title="Переместить вниз"
>
</button>
</div>
</div>
<div className="questions-modal-item-content">
<div className="questions-modal-item-text">{question.text}</div>
<div className="questions-modal-item-info">
@ -1208,19 +1394,34 @@ const GameManagementModal = ({
<div className="questions-modal-item-actions">
<button
className="questions-modal-edit-button"
onClick={() => handleEditQuestion(question)}
onClick={(e) => {
e.stopPropagation()
handleEditQuestion(question)
}}
onMouseDown={(e) => e.stopPropagation()}
title="Редактировать"
>
</button>
<button
className="questions-modal-delete-button"
onClick={() => handleDeleteQuestion(question.id)}
onClick={(e) => {
e.stopPropagation()
handleDeleteQuestion(question.id)
}}
onMouseDown={(e) => e.stopPropagation()}
title="Удалить"
>
×
</button>
</div>
<div
className="questions-modal-item-drag-handle"
title="Перетащите для изменения порядка"
onMouseDown={(e) => e.stopPropagation()}
>
</div>
</div>
))}
</div>

View file

@ -6,7 +6,7 @@ const UPDATE_INTERVAL = 500 // Check every 500ms
function createSnowflake(id, isInitial = false) {
return {
id: id || `snowflake-${Date.now()}-${Math.random()}`,
id: id || crypto.randomUUID(),
left: Math.random() * 100,
duration: Math.random() * 3 + 7, // 7-10s
delay: isInitial ? Math.random() * 2 : 0, // Only delay initial batch