1- // SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA < http://www.arduino.cc>
1+ // SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL ( http://www.arduino.cc)
22//
33// SPDX-License-Identifier: MPL-2.0
44
55const socket = io ( `http://${ window . location . host } ` ) ;
66
7+ let generateStoryButtonOriginalHTML = '' ; // To store the original content of the generate story button
8+ let storyBuffer = '' ;
9+
710
811
912function initSocketIO ( ) {
10- socket . on ( 'response' , ( data ) => {
11- document . getElementById ( 'story-container' ) . style . display = 'flex' ;
12- const storyResponse = document . getElementById ( 'story-response' ) ;
13- storyResponse . textContent = data ;
14- document . getElementById ( 'loading-spinner' ) . style . display = 'none' ;
15- const clearStoryButton = document . getElementById ( 'clear-story-button' ) ;
16- clearStoryButton . style . display = 'block' ;
17- clearStoryButton . disabled = false ;
13+ socket . on ( 'prompt' , ( data ) => {
14+ const promptContainer = document . getElementById ( 'prompt-container' ) ;
15+ const promptDisplay = document . getElementById ( 'prompt-display' ) ;
16+ promptDisplay . innerHTML = data ;
17+ promptContainer . style . display = 'block' ;
1818 } ) ;
19+
20+ socket . on ( 'response' , ( data ) => {
21+
22+ document . getElementById ( 'story-container' ) . style . display = 'flex' ;
23+
24+ storyBuffer += data ;
25+
26+ } ) ;
27+
28+
29+
30+ socket . on ( 'stream_end' , ( ) => {
31+
32+ const storyResponse = document . getElementById ( 'story-response' ) ;
33+
34+ storyResponse . innerHTML = storyBuffer ;
35+
36+
37+
38+ document . getElementById ( 'loading-spinner' ) . style . display = 'none' ;
39+
40+ const clearStoryButton = document . getElementById ( 'clear-story-button' ) ;
41+
42+ clearStoryButton . style . display = 'block' ;
43+
44+ clearStoryButton . disabled = false ;
45+
46+
47+
48+ const generateStoryButton = document . querySelector ( '.generate-story-button' ) ;
49+
50+ if ( generateStoryButton ) {
51+
52+ generateStoryButton . disabled = false ;
53+
54+ generateStoryButton . innerHTML = generateStoryButtonOriginalHTML ; // Restore original content
55+
56+ }
57+
58+ } ) ;
1959}
2060
2161function unlockAndOpenNext ( currentContainer ) {
@@ -33,6 +73,12 @@ function unlockAndOpenNext(currentContainer) {
3373 }
3474}
3575
76+ function getRandomElement ( elements ) {
77+ if ( elements . length === 0 ) return null ;
78+ const randomIndex = Math . floor ( Math . random ( ) * elements . length ) ;
79+ return elements [ randomIndex ] ;
80+ }
81+
3682function setupChipSelection ( container ) {
3783 const chips = container . querySelectorAll ( '.chip' ) ;
3884 const selectedValue = container . querySelector ( '.selected-value' ) ;
@@ -49,6 +95,13 @@ function setupChipSelection(container) {
4995 if ( ! alreadySelected ) {
5096 unlockAndOpenNext ( container ) ;
5197 }
98+
99+ // Collapse the current container
100+ const content = container . querySelector ( '.parameter-content' ) ;
101+ const arrow = container . querySelector ( '.arrow-icon' ) ;
102+ content . style . display = 'none' ;
103+ arrow . classList . remove ( 'rotated' ) ;
104+
52105 } ) ;
53106 } ) ;
54107}
@@ -129,6 +182,17 @@ function checkCharactersAndUnlockNext(charactersContainer) {
129182}
130183
131184function gatherDataAndGenerateStory ( ) {
185+ document . querySelectorAll ( '.parameter-container' ) . forEach ( container => {
186+ const content = container . querySelector ( '.parameter-content' ) ;
187+ if ( content && content . style . display === 'block' ) {
188+ content . style . display = 'none' ;
189+ const arrow = container . querySelector ( '.arrow-icon' ) ;
190+ if ( arrow ) {
191+ arrow . classList . remove ( 'rotated' ) ;
192+ }
193+ }
194+ } ) ;
195+
132196 const age = document . querySelector ( '.parameter-container:nth-child(1) .chip.selected' ) ?. textContent . trim ( ) || 'any' ;
133197 const theme = document . querySelector ( '.parameter-container:nth-child(2) .chip.selected' ) ?. textContent . trim ( ) || 'any' ;
134198 const storyTypeContainer = document . querySelector ( '.parameter-container:nth-child(3)' ) ;
@@ -168,9 +232,20 @@ function generateStory(data) {
168232 document . querySelector ( '.story-output-placeholder' ) . style . display = 'none' ;
169233 const responseArea = document . getElementById ( 'story-response-area' ) ;
170234 responseArea . style . display = 'flex' ;
235+ document . getElementById ( 'prompt-container' ) . style . display = 'none' ;
236+ document . getElementById ( 'prompt-display' ) . textContent = '' ;
171237 document . getElementById ( 'story-container' ) . style . display = 'none' ;
172- document . getElementById ( 'loading-spinner' ) . style . display = 'block' ;
173- document . getElementById ( 'story-response' ) . textContent = '' ;
238+ document . getElementById ( 'story-response' ) . innerHTML = '' ; // Use innerHTML to clear
239+ storyBuffer = '' ; // Reset buffer
240+ document . getElementById ( 'loading-spinner' ) . style . display = 'block' ; // Show the general loading spinner
241+
242+ const generateStoryButton = document . querySelector ( '.generate-story-button' ) ;
243+ if ( generateStoryButton ) {
244+ generateStoryButton . disabled = true ;
245+ // Append the spinner instead of replacing innerHTML
246+ generateStoryButton . innerHTML += '<div class="button-spinner spinner"></div>' ;
247+ }
248+
174249 document . getElementById ( 'clear-story-button' ) . style . display = 'none' ;
175250 socket . emit ( 'generate_story' , data ) ;
176251}
@@ -223,10 +298,12 @@ function resetStoryView() {
223298 }
224299 }
225300
226- // Hide "Generate story" button
301+ // Restore "Generate story" button to original state
227302 const generateStoryButton = document . querySelector ( '.generate-story-button' ) ;
228303 if ( generateStoryButton ) {
229- generateStoryButton . style . display = 'none' ;
304+ generateStoryButton . style . display = 'none' ; // Keep hidden if no chars, will be set to flex by checkCharactersAndUnlockNext
305+ generateStoryButton . disabled = false ;
306+ generateStoryButton . innerHTML = generateStoryButtonOriginalHTML ;
230307 }
231308
232309 // Reset parameter containers state
@@ -240,7 +317,9 @@ function resetStoryView() {
240317 arrow . classList . add ( 'rotated' ) ;
241318 container . classList . remove ( 'disabled' ) ;
242319 } else {
243- container . classList . add ( 'disabled' ) ;
320+ if ( container . id !== 'prompt-container' ) {
321+ container . classList . add ( 'disabled' ) ;
322+ }
244323 content . style . display = 'none' ;
245324 arrow . classList . remove ( 'rotated' ) ;
246325 }
@@ -250,6 +329,11 @@ function resetStoryView() {
250329document . addEventListener ( 'DOMContentLoaded' , ( ) => {
251330 initSocketIO ( ) ;
252331
332+ const generateStoryButton = document . querySelector ( '.generate-story-button' ) ;
333+ if ( generateStoryButton ) {
334+ generateStoryButtonOriginalHTML = generateStoryButton . innerHTML ; // Store original content
335+ }
336+
253337 const parameterContainers = document . querySelectorAll ( '.parameter-container' ) ;
254338
255339 parameterContainers . forEach ( ( container , index ) => {
@@ -259,7 +343,9 @@ document.addEventListener('DOMContentLoaded', () => {
259343 content . style . display = 'block' ;
260344 arrow . classList . add ( 'rotated' ) ;
261345 } else {
262- container . classList . add ( 'disabled' ) ;
346+ if ( container . id !== 'prompt-container' ) {
347+ container . classList . add ( 'disabled' ) ;
348+ }
263349 }
264350 } ) ;
265351
@@ -360,18 +446,77 @@ document.addEventListener('DOMContentLoaded', () => {
360446 } ) ;
361447
362448 document . getElementById ( 'copy-story-button' ) . addEventListener ( 'click' , ( ) => {
363- const storyText = document . getElementById ( 'story-response' ) . textContent ;
364- navigator . clipboard . writeText ( storyText ) . then ( ( ) => {
365- const copyButton = document . getElementById ( 'copy-story-button' ) ;
366- const originalHTML = copyButton . innerHTML ;
449+ const storyText = document . getElementById ( 'story-response' ) . innerText ;
450+ const copyButton = document . getElementById ( 'copy-story-button' ) ;
451+ const originalHTML = copyButton . innerHTML ;
452+ const textarea = document . createElement ( 'textarea' ) ;
453+ textarea . value = storyText ;
454+ document . body . appendChild ( textarea ) ;
455+ textarea . select ( ) ;
456+ try {
457+ document . execCommand ( 'copy' ) ;
367458 copyButton . textContent = 'Copied!' ;
368459 copyButton . disabled = true ;
369- setTimeout ( ( ) => {
370- copyButton . innerHTML = originalHTML ;
371- copyButton . disabled = false ;
372- } , 2000 ) ;
373- } , ( err ) => {
460+ } catch ( err ) {
374461 console . error ( 'Could not copy text: ' , err ) ;
375- } ) ;
462+ }
463+ document . body . removeChild ( textarea ) ;
464+
465+ setTimeout ( ( ) => {
466+ copyButton . innerHTML = originalHTML ;
467+ copyButton . disabled = false ;
468+ } , 2000 ) ;
469+ } ) ;
470+
471+ document . getElementById ( 'generate-randomly-button' ) . addEventListener ( 'click' , ( ) => {
472+ // Age
473+ const ageChips = document . querySelectorAll ( '.parameter-container:nth-child(1) .chip' ) ;
474+ const randomAgeChip = getRandomElement ( ageChips ) ;
475+ const age = randomAgeChip ? randomAgeChip . textContent . trim ( ) : 'any' ;
476+
477+ // Theme
478+ const themeChips = document . querySelectorAll ( '.parameter-container:nth-child(2) .chip' ) ;
479+ const randomThemeChip = getRandomElement ( themeChips ) ;
480+ const theme = randomThemeChip ? randomThemeChip . textContent . trim ( ) : 'any' ;
481+
482+ // Story Type
483+ const storyTypeContainer = document . querySelector ( '.parameter-container:nth-child(3)' ) ;
484+
485+ // Tone
486+ const toneChips = storyTypeContainer . querySelectorAll ( '.story-type-paragraph:nth-child(1) .chip' ) ;
487+ const randomToneChip = getRandomElement ( toneChips ) ;
488+ const tone = randomToneChip ? randomToneChip . textContent . trim ( ) : 'any' ;
489+
490+ // Ending type
491+ const endingTypeChips = storyTypeContainer . querySelectorAll ( '.story-type-paragraph:nth-child(2) .chip' ) ;
492+ const randomEndingTypeChip = getRandomElement ( endingTypeChips ) ;
493+ const endingType = randomEndingTypeChip ? randomEndingTypeChip . textContent . trim ( ) : 'any' ;
494+
495+ // Narrative structure
496+ const narrativeStructureChips = storyTypeContainer . querySelectorAll ( '.story-type-paragraph:nth-child(3) .chip' ) ;
497+ const randomNarrativeStructureChip = getRandomElement ( narrativeStructureChips ) ;
498+ const narrativeStructure = randomNarrativeStructureChip ? randomNarrativeStructureChip . textContent . trim ( ) : 'any' ;
499+
500+ // Duration
501+ const durationChips = storyTypeContainer . querySelectorAll ( '.story-type-paragraph:nth-child(4) .chip' ) ;
502+ const randomDurationChip = getRandomElement ( durationChips ) ;
503+ const duration = randomDurationChip ? randomDurationChip . textContent . trim ( ) : 'any' ;
504+
505+ // Characters and Other will be empty for random generation.
506+ const characters = [ ] ;
507+ const other = '' ;
508+
509+ const storyData = {
510+ age,
511+ theme,
512+ tone,
513+ endingType,
514+ narrativeStructure,
515+ duration,
516+ characters,
517+ other,
518+ } ;
519+
520+ generateStory ( storyData ) ;
376521 } ) ;
377522} ) ;
0 commit comments