11<script setup>
22import { computed , ref , onMounted , onUnmounted , nextTick } from ' vue' ;
33import { writeStreaming , rewriteStreaming } from ' ./ai-helpers' ;
4+ import { TextSelection } from ' prosemirror-state' ;
45import edit from ' @harbour-enterprises/common/icons/edit-regular.svg?raw' ;
56import paperPlane from ' @harbour-enterprises/common/icons/paper-plane-regular.svg?raw' ;
6- import timesCircle from ' @harbour-enterprises/common/icons/times-circle-regular.svg?raw' ;
7- import sun from ' @harbour-enterprises/common/icons/sun-regular.svg?raw' ;
87
98const props = defineProps ({
109 selectedText: {
@@ -37,6 +36,8 @@ const props = defineProps({
3736 },
3837});
3938
39+ const emits = defineEmits ([' ai-highlight' ]);
40+
4041// Store the selection state
4142const selectionState = ref (null );
4243
@@ -45,6 +46,10 @@ const aiWriterRef = ref(null);
4546
4647const handleClickOutside = (event ) => {
4748 if (aiWriterRef .value && ! aiWriterRef .value .contains (event .target )) {
49+ // Only emit 'remove' if we're not in a loading state
50+ if (! isLoading .value ) {
51+ emitAiHighlight (' remove' );
52+ }
4853 props .handleClose ();
4954 }
5055};
@@ -53,16 +58,25 @@ const handleClickOutside = (event) => {
5358const editableRef = ref (null );
5459
5560// Helper function to emit AI highlight events
61+ // We need to emit through the superToolbar if it exists, otherwise emit through the emits
62+ // Hoping we can simplify this logic in the future if we combine SE and SD
5663const emitAiHighlight = (type , data = null ) => {
5764 if (props .superToolbar ) {
5865 props .superToolbar .emit (' ai-highlight' , { type, data });
66+ } else {
67+ emits (' ai-highlight' , { type, data });
5968 }
6069};
6170
6271// Helper functions
6372const saveSelection = () => {
6473 if (props .selectedText ) {
65- selectionState .value = props .editor .state .selection ;
74+ // Store the complete selection state
75+ selectionState .value = {
76+ ... props .editor .state .selection ,
77+ from: props .editor .state .selection .from ,
78+ to: props .editor .state .selection .to
79+ };
6680 // Store the selection in the editor's state
6781 props .editor .commands .setMeta (' storedSelection' , selectionState .value );
6882
@@ -97,9 +111,10 @@ onMounted(() => {
97111});
98112
99113onUnmounted (() => {
100- // emit the ai highlight remove event through the toolbar
101- emitAiHighlight (' remove' );
102-
114+ // Only emit 'remove' if we're not in a loading state
115+ if (! isLoading .value ) {
116+ emitAiHighlight (' remove' );
117+ }
103118 // Remove all event listeners
104119 removeEventListeners ();
105120});
@@ -123,6 +138,8 @@ const placeholderText = computed(() =>
123138const isLoading = ref (false );
124139const isError = ref (' ' );
125140const promptText = ref (' ' );
141+ const textProcessingStarted = ref (false );
142+ const previousText = ref (' ' );
126143
127144// Computed property to check if editor is in suggesting mode
128145const isInSuggestingMode = computed (() => {
@@ -144,10 +161,39 @@ const getDocumentXml = () => {
144161// Handler for processing text chunks from the stream
145162const handleTextChunk = (text ) => {
146163 try {
147- // If this is the first chunk and we're rewriting, remove the selected text
164+ // Remove the loader node when we start receiving text
165+ props .editor .commands .removeAiNode (' aiLoaderNode' );
166+
167+ // If this is the first chunk and we're rewriting, handle the selection
148168 if (props .selectedText && ! textProcessingStarted .value ) {
169+ emitAiHighlight (' remove' );
170+
171+ // Clear the pulsing animation when we start inserting text
172+ props .editor .commands .clearAiHighlightStyle ();
173+
174+ // Check if we have a valid stored selection
175+ if (selectionState .value ) {
176+ // Apply the stored selection using the TextSelection API
177+ const { state } = props .editor ;
178+ const { from , to } = selectionState .value ;
179+
180+ // Create a transaction to set the selection
181+ const tr = state .tr .setSelection (
182+ TextSelection .create (state .doc , from, to)
183+ );
184+
185+ // Dispatch the transaction to update the editor state
186+ props .editor .view .dispatch (tr);
187+ } else {
188+ console .warn (' [AIWriter] No stored selection to restore' );
189+ }
190+
191+ // Now delete the selection
149192 props .editor .commands .deleteSelection ();
193+
194+ // Mark as processed
150195 textProcessingStarted .value = true ;
196+
151197 }
152198
153199 // If the text is null, undefined or empty, don't process it
@@ -188,22 +234,33 @@ const handleDone = () => {
188234 // We need to wait for the animation to finish before removing the mark
189235 setTimeout (() => {
190236 props .editor .commands .removeAiMark (' aiAnimationMark' );
237+ // Remove the highlight when we're done
238+ emitAiHighlight (' remove' );
191239 }, 1000 );
192240};
193241
194- // Track text processing state
195- const textProcessingStarted = ref (false );
196- const previousText = ref (' ' );
197-
198242// Refactored handleSubmit function
199243const handleSubmit = async () => {
200244 // Reset state
201- isLoading .value = true ;
202245 isError .value = ' ' ;
203246 textProcessingStarted .value = false ;
204247 previousText .value = ' ' ;
248+ isLoading .value = true ; // Set loading to true before starting the operation
205249
206250 try {
251+ // Close the AI Writer immediately and transition to loading states
252+ props .handleClose ();
253+
254+ // If there is selected text, emit the update event to start pulsing animation
255+ if (props .selectedText ) {
256+ emitAiHighlight (' update' );
257+ } else {
258+ // Insert the loader node at the current cursor position
259+ props .editor .commands .insertContent ({
260+ type: ' aiLoaderNode' ,
261+ });
262+ }
263+
207264 // Enable track changes if in suggesting mode
208265 if (isInSuggestingMode .value ) {
209266 props .editor .commands .enableTrackChanges ();
@@ -233,18 +290,19 @@ const handleSubmit = async () => {
233290 await writeStreaming (promptText .value , options, handleTextChunk, handleDone);
234291 }
235292
236- // If all is good, close the AI Writer
237- props .handleClose ();
238293 } catch (error) {
239294 console .error (' AI generation error:' , error);
240295 isError .value = error .message || ' An error occurred' ;
241296 } finally {
242- promptText .value = ' ' ; // Clear the input after submission
297+ // Clear the input after submission
298+ promptText .value = ' ' ;
243299 // Only disable track changes if we enabled it (in suggesting mode)
244300 if (isInSuggestingMode .value ) {
245301 props .editor .commands .disableTrackChanges ();
246302 }
247303 isLoading .value = false ;
304+ // Ensure textProcessingStarted is reset for next operation
305+ textProcessingStarted .value = false ;
248306 }
249307};
250308
@@ -283,11 +341,7 @@ const handleInput = (event) => {
283341 >< / textarea>
284342 < / div>
285343 < div class = " ai-loader" >
286- < span v- if = " isLoading" class = " ai-textarea-icon loading spinner-wrapper" >
287- < span v- html= " sun" >< / span>
288- < / span>
289- < span v- else - if = " isError" class = " ai-textarea-icon error" v- html= " timesCircle" / >
290- < span v- else - if = " promptText" class = " ai-textarea-icon ai-submit-button" @click .stop = " handleSubmit" v- html= " paperPlane" / >
344+ < span v- if = " promptText" class = " ai-textarea-icon ai-submit-button" @click .stop = " handleSubmit" v- html= " paperPlane" / >
291345 < / div>
292346 < / div>
293347< / template>
0 commit comments