I am currently trying to build my first ever game. I had some great progress in the first few weeks, but now I want to improve some things that are annoying me. My major pain point right now is the dialog system.
In my project I have NPCs that the player can interact with if they come close. To achieve this I use the body_entered and body_exited signals in the NPC's script like this:
func _on_worker_talking_area_body_entered(body):
  if body.name == "Player":
    $BubbleAnimation.show()
    $BubbleAnimation.play("on")
    $DialogLayer/Dialog/Panel/DialogText.text = approach_text       
    $DialogLayer.show()
    
    
func _on_worker_talking_area_body_exited(body):
  if body.name == "Player":
    $BubbleAnimation.stop()
    $BubbleAnimation.hide()
    $DialogLayer.hide()`

This works nicely, so that when the player comes near an NPC a little bubble appears. Now the player can interact using different buttons ("space", "y" and "n") to trigger a dialog depending on the state of the NPC. I get these inputs like this:
func _input(event: InputEvent):
  if event.is_action_pressed("interaction") and $BubbleAnimation.visible:
    if worker_name == "welcome_worker" and state == 0:
        await $DialogLayer.split_text(get_current_text())
        state = 1
        state_changed.emit()
    
    if (worker_name == "crane_worker" or worker_name == "milling_machine_worker") \
    and state == 1:
        await $DialogLayer.split_text(get_current_text())
        state = 2 
    
    if (worker_name == "crane_worker" or worker_name == "milling_machine_worker") \
    and state == 2:
        await $DialogLayer.split_text(get_current_text())       
    
  if event.is_action_pressed("no") and state == 2 and $BubbleAnimation.visible:
    state = 3
    await $DialogLayer.split_text(get_current_text())
    state_changed.emit()

  if event.is_action_pressed("yes") and state == 2 and $BubbleAnimation.visible:
    state = 4
    await $DialogLayer.split_text(get_current_text())
    state_changed.emit()`

The text is then displayed in the DialogLayer which displays it by scrolling letter by letter:
func split_text(input_text: String) -> void:
   $Dialog.show()
   var text_array = input_text.split("/")
   for text_piece in text_array:
      scroll_text(text_piece)
      await get_tree().create_timer(5.5).timeout
   $Dialog.hide()

func scroll_text(input_text: String) -> void:
   var text = input_text
   var visible_characters = 0
   for i in text.length():
      visible_characters += 1
      await get_tree().create_timer(0.1).timeout
      $Dialog/Panel/DialogText.text = text.left(visible_characters)`

Now there are several things that don't work properly here:

The player can leave the "talking area" while the text is scrolling, which will stop displaying the text, but the scrolling continues without the player seeing it.
It is super annoying to wait for 5.5 seconds for each line of dialog, if that line of dialog is super short. So it would be nice to give the player the ability to skip to the end using space ("interaction").
Sometimes this whole set-up seems to fail completely for reasons I just can't figure out. In this case the dialog just disappears suddenly.

So I was wondering how could I set up such a dialog system properly? What are some guidelines or tips how to get this working? I would really apreciate every input, as I am quite stuck right now.