@@ -20,9 +20,11 @@ import type {
2020 ConversationsEnhancedRichTextInputProps as RichTextInputProps ,
2121} from './conversations-enhanced-rich-text-input.types' ;
2222import {
23- getTokenIcon ,
24- getTokenTypeLabel ,
2523 adjustTokenSpacing as adjustTokenSpacingHelper ,
24+ getTokenDisplayInfo ,
25+ serializeTokensForSend ,
26+ renderRichTextHtml ,
27+ triggerCharForTokenType ,
2628} from './conversations-enhanced-rich-text-helpers' ;
2729
2830export default function RichTextInput ( {
@@ -217,13 +219,7 @@ export default function RichTextInput({
217219 const insertToken = ( token : Token ) => {
218220 if ( ! textareaRef . current ) return ;
219221
220- const triggerChar =
221- token . type === 'evidence_document' ? '^' :
222- token . type === 'shippable' ? '@' :
223- token . type === 'attachment' ? '+' :
224- token . type === 'source' ? '#' :
225- token . type === 'destination' ? '!' :
226- token . type === 'command' ? ':' : '!' ;
222+ const triggerChar = triggerCharForTokenType ( token . type ) ;
227223
228224 // Find the last occurrence of the trigger character before cursor
229225 const lastTriggerIndex = text . substring ( 0 , cursorPosition ) . lastIndexOf ( triggerChar ) ;
@@ -291,107 +287,12 @@ export default function RichTextInput({
291287 setActivePicker ( null ) ;
292288 } ;
293289
294- // Helper function to get additional display information for tokens
295- const getTokenDisplayInfo = ( token : Token ) => {
296- switch ( token . type ) {
297- case 'evidence_document' :
298- return token . data ?. description ? token . data . description . substring ( 0 , 30 ) : '' ;
299- case 'shippable' :
300- return token . data ?. status ? token . data . status : '' ;
301- case 'attachment' :
302- return token . data ?. size ? token . data . size : '' ;
303- case 'source' :
304- return token . data ?. provider ? `${ token . data . provider } • ${ token . data . path } ` : token . data ?. path || '' ;
305- case 'command' :
306- return token . data ?. shortcut ? token . data . shortcut : '' ;
307- case 'destination' :
308- case 'pipeline_run' :
309- if ( ! token . data ?. pipelineType ) return '' ;
310- if ( String ( token . data . pipelineType ) . toLowerCase ( ) . includes ( 'measure' ) ) return 'read-measurement' ;
311- if (
312- String ( token . data . pipelineType ) . toLowerCase ( ) . includes ( 'asset-pack' ) ||
313- String ( token . data . pipelineType ) . toLowerCase ( ) . includes ( 'shippable' ) ||
314- String ( token . data . pipelineType ) . toLowerCase ( ) . includes ( 'artifact' )
315- ) {
316- return 'branch-artifact' ;
317- }
318- return `${ token . data . pipelineType } ` ;
319- default :
320- return '' ;
321- }
322- } ;
290+ // getTokenDisplayInfo imported from helpers
323291
324292 // Handle send message
325293 const handleSend = ( ) => {
326294 if ( ! text . trim ( ) ) return ;
327-
328- // Clean up tokens before sending
329- // This ensures we only send tokens that are actually in the text
330- const validTokens = tokens . filter ( token => text . includes ( token . text ) ) ;
331-
332- const serializedTokens = validTokens . map ( ( token ) => {
333- if ( token . type === 'source' ) {
334- return {
335- ...token ,
336- value : token . text . trim ( ) ,
337- metadata : {
338- attachment_id : token . data ?. id || token . data ?. repoId || token . data ?. path || token . text . trim ( ) ,
339- category : 'integration' ,
340- type : token . data ?. type || 'github_repo' ,
341- ...token . data ,
342- } ,
343- } ;
344- }
345-
346- if ( token . type === 'attachment' ) {
347- return {
348- ...token ,
349- value : token . text . trim ( ) ,
350- metadata : {
351- attachment_id : token . data ?. id || token . data ?. path || token . text . trim ( ) ,
352- category : token . data ?. category || 'file' ,
353- type : token . data ?. type || 'attachment' ,
354- ...token . data ,
355- } ,
356- } ;
357- }
358-
359- if ( token . type === 'destination' || token . type === 'pipeline_run' ) {
360- return {
361- ...token ,
362- type : 'destination' as const ,
363- value : token . text . trim ( ) ,
364- metadata : {
365- attachment_id : token . data ?. pipelineId || token . data ?. id || token . text . trim ( ) ,
366- category : token . data ?. category || 'integration' ,
367- type : token . data ?. type || 'output_destination' ,
368- ...token . data ,
369- } ,
370- } ;
371- }
372-
373- if ( token . type === 'shippable' ) {
374- return {
375- ...token ,
376- type : 'shippable' ,
377- value : token . text . trim ( ) ,
378- metadata : {
379- kind : 'shippable' ,
380- asset_pack_reference : token . data ?. id || null ,
381- ...token . data ,
382- } ,
383- } ;
384- }
385-
386- return {
387- ...token ,
388- value : token . text . trim ( ) ,
389- metadata : {
390- ...token . data ,
391- } ,
392- } ;
393- } ) ;
394-
295+ const serializedTokens = serializeTokensForSend ( tokens , text ) ;
395296 onSend ( text , serializedTokens as Token [ ] ) ;
396297 setText ( '' ) ;
397298 setTokens ( [ ] ) ;
@@ -421,64 +322,9 @@ export default function RichTextInput({
421322 fileInput . click ( ) ;
422323 } ;
423324
424- // In RichTextInput component, memoize the renderRichText function
425325 const renderRichText = useCallback ( ( ) => {
426- // Always return the text, even if there are no tokens
427- // This ensures the overlay always shows something
428- if ( ! text ) return '' ;
429-
430- // Escape user input to prevent HTML injection
431- let result = text
432- . replace ( / & / g, '&' )
433- . replace ( / < / g, '<' )
434- . replace ( / > / g, '>' ) ;
435-
436- // If no tokens, just return the plain text
437- if ( tokens . length === 0 ) {
438- return result ;
439- }
440-
441- // Sort tokens by their position in the text (to avoid replacement conflicts)
442- // Sort by length (longest first) to avoid replacing parts of longer tokens
443- const sortedTokens = [ ...tokens ] . sort ( ( a , b ) => {
444- // First sort by position
445- const posA = result . indexOf ( a . text ) ;
446- const posB = result . indexOf ( b . text ) ;
447-
448- if ( posA !== posB ) return posA - posB ;
449-
450- // If positions are the same, sort by length (longest first)
451- return b . text . length - a . text . length ;
452- } ) ;
453-
454- // Process each token
455- sortedTokens . forEach ( token => {
456- // Escape special regex characters in the token text
457- const escapedText = token . text . trim ( ) . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
458-
459- // Create a regex that matches the token text precisely
460- // Using word boundaries to ensure we match whole tokens
461- const regex = new RegExp ( `(^|\\s)(${ escapedText } )(?=\\s|$)` , 'g' ) ;
462-
463- // Get the appropriate icon for the token type
464- const iconHtml = getTokenIcon ( token . type ) ;
465-
466- // Get the label for the token type
467- const typeLabel = getTokenTypeLabel ( token . type ) ;
468-
469- // Replace the token text with the styled version
470- // Use a simpler token structure with fewer nested elements and whitespace
471- result = result . replace ( regex , ( match , before , tokenText ) => {
472- // Include additional information if available
473- const infoHtml = token . displayInfo ?
474- `<span class="token-info">${ token . displayInfo } </span>` : '' ;
475-
476- return `${ before } <span class="token token-${ token . type } " title="${ typeLabel } : ${ tokenText } ${ token . displayInfo ? ' - ' + token . displayInfo : '' } ">${ iconHtml } ${ tokenText } ${ infoHtml } </span>` ;
477- } ) ;
478- } ) ;
479-
480- return result ;
481- } , [ text , tokens , cursorPosition ] ) ;
326+ return renderRichTextHtml ( text , tokens ) ;
327+ } , [ text , tokens ] ) ;
482328
483329 // Sync the rich text overlay whenever text or tokens change
484330 useEffect ( ( ) => {
0 commit comments