animationCoordinator.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. // Animation Coordinator - Centralized state management for all animations and scrolling
  2. import { readonly, ref, watch } from 'vue'
  3. export interface AnimationState {
  4. messageStreaming: boolean
  5. messageTyping: boolean
  6. titleAnimating: boolean
  7. scrolling: boolean
  8. }
  9. class AnimationCoordinator {
  10. private state = ref<AnimationState>({
  11. messageStreaming: false,
  12. messageTyping: false,
  13. titleAnimating: false,
  14. scrolling: false,
  15. })
  16. private callbacks: {
  17. onMessageTypingComplete?: () => void
  18. onTitleAnimationComplete?: () => void
  19. onAllAnimationsComplete?: () => void
  20. } = {}
  21. // Get current state (readonly)
  22. getState() {
  23. return readonly(this.state)
  24. }
  25. // Check if any animation is in progress
  26. isAnyAnimationActive() {
  27. const s = this.state.value
  28. return s.messageStreaming || s.messageTyping || s.titleAnimating || s.scrolling
  29. }
  30. // Check if message-related animations are complete
  31. isMessageAnimationComplete() {
  32. const s = this.state.value
  33. return !s.messageStreaming && !s.messageTyping
  34. }
  35. // Set message streaming state
  36. setMessageStreaming(streaming: boolean) {
  37. if (this.state.value.messageStreaming === streaming)
  38. return
  39. this.state.value.messageStreaming = streaming
  40. if (!streaming) {
  41. // When streaming stops, message typing might still be active
  42. this.checkTransitions()
  43. }
  44. }
  45. // Set message typing state
  46. setMessageTyping(typing: boolean) {
  47. // Prevent redundant state changes
  48. if (this.state.value.messageTyping === typing)
  49. return
  50. this.state.value.messageTyping = typing
  51. if (!typing) {
  52. this.callbacks.onMessageTypingComplete?.()
  53. this.checkTransitions()
  54. }
  55. }
  56. // Set title animation state
  57. setTitleAnimating(animating: boolean) {
  58. if (this.state.value.titleAnimating === animating)
  59. return
  60. this.state.value.titleAnimating = animating
  61. if (!animating) {
  62. this.callbacks.onTitleAnimationComplete?.()
  63. this.checkTransitions()
  64. }
  65. }
  66. // Set scrolling state
  67. setScrolling(scrolling: boolean) {
  68. this.state.value.scrolling = scrolling
  69. if (!scrolling) {
  70. this.checkTransitions()
  71. }
  72. }
  73. // Set callbacks
  74. setCallbacks(callbacks: Partial<typeof this.callbacks>) {
  75. Object.assign(this.callbacks, callbacks)
  76. }
  77. private titleAnimationTriggered = false
  78. // Check for state transitions and trigger appropriate actions
  79. private checkTransitions() {
  80. const s = this.state.value
  81. // If message animation is complete and title is not animating, we can start title animation
  82. if (this.isMessageAnimationComplete() && !s.titleAnimating && !this.titleAnimationTriggered) {
  83. this.titleAnimationTriggered = true
  84. // Small delay before starting title animation
  85. setTimeout(() => {
  86. if (this.isMessageAnimationComplete() && !this.state.value.titleAnimating) {
  87. this.triggerTitleAnimation()
  88. }
  89. }, 200)
  90. }
  91. // If all animations are complete
  92. if (!this.isAnyAnimationActive()) {
  93. this.callbacks.onAllAnimationsComplete?.()
  94. }
  95. }
  96. // Trigger title animation (to be called by external code)
  97. private triggerTitleAnimation() {
  98. // This will be handled by the LLM store
  99. window.dispatchEvent(new CustomEvent('startTitleAnimation'))
  100. }
  101. // Reset all states (useful when starting a new conversation)
  102. reset() {
  103. this.state.value = {
  104. messageStreaming: false,
  105. messageTyping: false,
  106. titleAnimating: false,
  107. scrolling: false,
  108. }
  109. this.titleAnimationTriggered = false
  110. }
  111. // Wait for message animation to complete
  112. async waitForMessageAnimationComplete(): Promise<void> {
  113. return new Promise(resolve => {
  114. if (this.isMessageAnimationComplete()) {
  115. resolve()
  116. return
  117. }
  118. const unwatch = watch(
  119. () => this.isMessageAnimationComplete(),
  120. complete => {
  121. if (complete) {
  122. unwatch()
  123. resolve()
  124. }
  125. },
  126. )
  127. })
  128. }
  129. // Wait for all animations to complete
  130. async waitForAllAnimationsComplete(): Promise<void> {
  131. return new Promise(resolve => {
  132. if (!this.isAnyAnimationActive()) {
  133. resolve()
  134. return
  135. }
  136. const unwatch = watch(
  137. () => this.isAnyAnimationActive(),
  138. active => {
  139. if (!active) {
  140. unwatch()
  141. resolve()
  142. }
  143. },
  144. )
  145. })
  146. }
  147. }
  148. // Global singleton instance
  149. export const animationCoordinator = new AnimationCoordinator()
  150. // Composable for using in components
  151. export function useAnimationCoordinator() {
  152. return {
  153. coordinator: animationCoordinator,
  154. state: animationCoordinator.getState(),
  155. isAnyAnimationActive: () => animationCoordinator.isAnyAnimationActive(),
  156. isMessageAnimationComplete: () => animationCoordinator.isMessageAnimationComplete(),
  157. }
  158. }