1- import React , { useEffect , useState } from 'react' ;
1+ import React , { useEffect , useState , useMemo } from 'react' ;
22import MarkdownComponent from 'react-markdown'
33import remarkGfm from 'remark-gfm'
44import './ChatContent.css'
@@ -10,7 +10,7 @@ import { renderString } from '../../helpers/templatize'
1010import { getOrigin , getParsedIframeInfo } from '../../helpers/origin'
1111import { getApp } from '../../helpers/app'
1212import { getAllTemplateTagsInQuery , replaceLLMFriendlyIdentifiersInSqlWithModels } from 'apps'
13- import type { MetabaseModel } from 'apps/types'
13+ import type { MetabaseModel , MetabaseContext } from 'apps/types'
1414import { type EmbedConfigs } from '../../state/configs/reducer'
1515import { Badge } from "@chakra-ui/react" ;
1616import { CodeBlock } from './CodeBlock' ;
@@ -19,6 +19,7 @@ import { BsBarChartFill } from "react-icons/bs";
1919import { dispatch } from '../../state/dispatch' ;
2020import { updateIsDevToolsOpen } from '../../state/settings/reducer' ;
2121import { setMinusxMode } from '../../app/rpc' ;
22+ import { createMentionItems , MentionItem } from '../../helpers/mentionUtils' ;
2223
2324
2425function LinkRenderer ( props : any ) {
@@ -83,10 +84,60 @@ function ModifiedCode(props: any) {
8384 const text = props . children ?. toString ( ) || '' ;
8485
8586 if ( text . startsWith ( '[badge]' ) ) {
86- return < Badge color = { "minusxGreen.600" } aria-label = 'mx-badge' > { text . replace ( '[badge]' , '' ) } </ Badge > ;
87+ return < Badge color = { "minusxGreen.600" } aria-label = 'mx-badge' > { text . replace ( '[badge]' , '' ) } </ Badge > ;
8788 }
8889 if ( text . startsWith ( '[badge_mx]' ) ) {
89- return < > < br > </ br > < Badge aria-label = 'mx-badge' borderLeftColor = { "minusxGreen.600" } borderLeft = { "2px solid" } color = { "minusxGreen.600" } fontSize = { "sm" } mt = { 2 } > { text . replace ( '[badge_mx]' , '' ) } </ Badge > < br > </ br > </ > ;
90+ return < > < br > </ br > < Badge aria-label = 'mx-badge' borderLeftColor = { "minusxGreen.600" } borderLeft = { "2px solid" } color = { "minusxGreen.600" } fontSize = { "sm" } mt = { 2 } > { text . replace ( '[badge_mx]' , '' ) } </ Badge > < br > </ br > </ > ;
91+ }
92+ if ( text . startsWith ( '[mention:table:' ) ) {
93+ const tableName = text . replace ( '[mention:table:' , '' ) . replace ( ']' , '' ) ;
94+ const mentionItem = ( props as any ) . mentionItems ?. find ( ( item : MentionItem ) =>
95+ item . name === tableName && item . type === 'table'
96+ ) ;
97+
98+ let tooltipText = '' ;
99+ if ( mentionItem ) {
100+ tooltipText = `Name: ${ mentionItem . originalName } ` ;
101+ if ( mentionItem . schema ) tooltipText += ` | Schema: ${ mentionItem . schema } ` ;
102+ }
103+
104+ return (
105+ < Tooltip label = { tooltipText } placement = "top" hasArrow >
106+ < span style = { { color : '#3182ce' , fontWeight : 500 } } >
107+ @{ tableName }
108+ </ span >
109+ </ Tooltip >
110+ ) ;
111+ }
112+ if ( text . startsWith ( '[mention:model:' ) ) {
113+ const modelName = text . replace ( '[mention:model:' , '' ) . replace ( ']' , '' ) ;
114+ const mentionItem = ( props as any ) . mentionItems ?. find ( ( item : MentionItem ) =>
115+ item . name === modelName && item . type === 'model'
116+ ) ;
117+
118+ let tooltipText = '' ;
119+ if ( mentionItem ) {
120+ tooltipText = `Name: ${ mentionItem . originalName } ` ;
121+ if ( mentionItem . collection ) tooltipText += ` | Collection: ${ mentionItem . collection } ` ;
122+ }
123+
124+ return (
125+ < Tooltip label = { tooltipText } placement = "top" hasArrow >
126+ < span style = { { color : '#805ad5' , fontWeight : 500 } } >
127+ @{ modelName }
128+ </ span >
129+ </ Tooltip >
130+ ) ;
131+ }
132+ if ( text . startsWith ( '[mention:missing:' ) ) {
133+ const parts = text . replace ( '[mention:missing:' , '' ) . replace ( ']' , '' ) . split ( ':' ) ;
134+ const type = parts [ 0 ] ;
135+ const id = parts [ 1 ] ;
136+ return (
137+ < span style = { { color : '#718096' } } >
138+ @[{ type } :{ id } ]
139+ </ span >
140+ ) ;
90141 }
91142 }
92143
@@ -551,6 +602,90 @@ function extractLastQueryFromMessages(messages: any[], currentMessageIndex: numb
551602
552603const useAppStore = getApp ( ) . useStore ( ) ;
553604
605+ // Component to detect @ mentions in text and render with colors
606+ function MentionAwareText ( { children } : { children : React . ReactNode } ) {
607+ const toolContext : MetabaseContext = useAppStore ( ( state ) => state . toolContext )
608+
609+ // Create mention items for determining colors
610+ const mentionItems = useMemo ( ( ) => {
611+ if ( ! toolContext ?. dbInfo ) return [ ]
612+ const tables = toolContext . dbInfo . tables || [ ]
613+ const models = toolContext . dbInfo . models || [ ]
614+ return createMentionItems ( tables , models )
615+ } , [ toolContext ?. dbInfo ] )
616+
617+ // Create lookup map for determining mention types
618+ const mentionMap = useMemo ( ( ) => {
619+ const map = new Map < string , 'table' | 'model' > ( )
620+ mentionItems . forEach ( ( item : MentionItem ) => {
621+ map . set ( item . name . toLowerCase ( ) , item . type )
622+ } )
623+ return map
624+ } , [ mentionItems ] )
625+
626+ // Only process string children
627+ if ( typeof children !== 'string' ) {
628+ return < > { children } </ >
629+ }
630+
631+ const renderTextWithColors = ( text : string ) => {
632+ // Regex to find @ mentions (@ followed by word characters)
633+ const mentionRegex = / @ ( \w + ) / g
634+ const parts : ( string | JSX . Element ) [ ] = [ ]
635+ let lastIndex = 0
636+ let match
637+
638+ while ( ( match = mentionRegex . exec ( text ) ) !== null ) {
639+ // Add text before the mention
640+ if ( match . index > lastIndex ) {
641+ parts . push ( text . slice ( lastIndex , match . index ) )
642+ }
643+
644+ const mentionName = match [ 1 ]
645+ const mentionType = mentionMap . get ( mentionName . toLowerCase ( ) )
646+
647+ if ( mentionType ) {
648+ // Render as colored mention
649+ const color = mentionType === 'table' ? '#3182ce' : '#805ad5' // Blue for tables, purple for models
650+ parts . push (
651+ < span
652+ key = { `mention-${ match . index } ` }
653+ style = { { color, fontWeight : 500 } }
654+ >
655+ @{ mentionName }
656+ </ span >
657+ )
658+ } else {
659+ // Check if it looks like a missing reference pattern @[type:id]
660+ if ( text . slice ( match . index ) . match ( / ^ @ \[ [ ^ : ] + : \w + \] / ) ) {
661+ parts . push (
662+ < span
663+ key = { `mention-missing-${ match . index } ` }
664+ style = { { color : '#718096' } }
665+ >
666+ { match [ 0 ] }
667+ </ span >
668+ )
669+ } else {
670+ // Regular @ mention, not in our database
671+ parts . push ( match [ 0 ] )
672+ }
673+ }
674+
675+ lastIndex = match . index + match [ 0 ] . length
676+ }
677+
678+ // Add remaining text
679+ if ( lastIndex < text . length ) {
680+ parts . push ( text . slice ( lastIndex ) )
681+ }
682+
683+ return parts . length > 1 ? < > { parts } </ > : text
684+ }
685+
686+ return < > { renderTextWithColors ( children ) } </ >
687+ }
688+
554689export function Markdown ( { content, messageIndex} : { content : string , messageIndex ?: number } ) {
555690 const currentThread = useSelector ( ( state : RootState ) =>
556691 state . chat . threads [ state . chat . activeThread ]
@@ -566,6 +701,14 @@ export function Markdown({content, messageIndex}: {content: string, messageIndex
566701 const mxModels = useSelector ( ( state : RootState ) => state . cache . mxModels ) ;
567702
568703 // Process template variables like {{MX_LAST_QUERY_URL}}
704+ // Create mention items from toolContext
705+ const mentionItems = useMemo ( ( ) => {
706+ if ( ! toolContext ?. dbInfo ) return [ ]
707+ const tables = toolContext . dbInfo . tables || [ ]
708+ const models = toolContext . dbInfo . models || [ ]
709+ return createMentionItems ( tables , models )
710+ } , [ toolContext ?. dbInfo ] )
711+
569712 const processedContent = React . useMemo ( ( ) => {
570713 if ( content . includes ( '{{MX_LAST_QUERY_URL}}' ) ) {
571714 try {
@@ -594,7 +737,38 @@ export function Markdown({content, messageIndex}: {content: string, messageIndex
594737 return content ;
595738 } , [ content , currentThread ?. messages , toolContext ?. dbId , messageIndex , settings , mxModels ] ) ;
596739
740+ // Create a wrapped ModifiedCode component that has access to mentionItems
741+ const ModifiedCodeWithMentions = ( props : any ) => (
742+ < ModifiedCode { ...props } mentionItems = { mentionItems } />
743+ )
744+
597745 return (
598- < MarkdownComponent remarkPlugins = { [ remarkGfm ] } className = { "markdown" } components = { { a : LinkRenderer , p : ModifiedParagraph , ul : ModifiedUL , ol : ModifiedOL , img : ImageComponent , pre : ModifiedPre , blockquote : ModifiedBlockquote , code : ModifiedCode , hr : HorizontalLine , h1 : ModifiedH1 , h2 : ModifiedH2 , h3 : ModifiedH3 , h4 : ModifiedH4 , table : ModifiedTable , thead : ModifiedThead , tbody : ModifiedTbody , tr : ModifiedTr , th : ModifiedTh , td : ModifiedTd } } > { processedContent } </ MarkdownComponent >
746+ < MarkdownComponent
747+ remarkPlugins = { [ remarkGfm ] }
748+ className = { "markdown" }
749+ components = { {
750+ a : LinkRenderer ,
751+ p : ModifiedParagraph ,
752+ ul : ModifiedUL ,
753+ ol : ModifiedOL ,
754+ img : ImageComponent ,
755+ pre : ModifiedPre ,
756+ blockquote : ModifiedBlockquote ,
757+ code : ModifiedCodeWithMentions ,
758+ hr : HorizontalLine ,
759+ h1 : ModifiedH1 ,
760+ h2 : ModifiedH2 ,
761+ h3 : ModifiedH3 ,
762+ h4 : ModifiedH4 ,
763+ table : ModifiedTable ,
764+ thead : ModifiedThead ,
765+ tbody : ModifiedTbody ,
766+ tr : ModifiedTr ,
767+ th : ModifiedTh ,
768+ td : ModifiedTd ,
769+ } }
770+ >
771+ { processedContent }
772+ </ MarkdownComponent >
599773 )
600774}
0 commit comments