11'use client'
22
3- import { createElement , useState } from 'react'
3+ import { createElement , useMemo , useState } from 'react'
44import { useParams } from 'next/navigation'
55import { ArrowRight , ChevronDown , Expandable , ExpandableContent } from '@/components/emcn'
66import { cn } from '@/lib/core/utils/cn'
77import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
8+ import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
9+ import type {
10+ ChatMessageContext ,
11+ MothershipResource ,
12+ } from '@/app/workspace/[workspaceId]/home/types'
13+ import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
14+ import { useTablesList } from '@/hooks/queries/tables'
15+ import { useWorkflows } from '@/hooks/queries/workflows'
16+ import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
817
918export interface OptionsItemData {
1019 title : string
@@ -55,20 +64,32 @@ export interface FileTagData {
5564 content : string
5665}
5766
67+ export const WORKSPACE_RESOURCE_TAG_TYPES = [ 'workflow' , 'table' , 'file' ] as const
68+
69+ export type WorkspaceResourceTagType = ( typeof WORKSPACE_RESOURCE_TAG_TYPES ) [ number ]
70+
71+ export interface WorkspaceResourceTagData {
72+ type : WorkspaceResourceTagType
73+ id : string
74+ title ?: string
75+ }
76+
5877export type ContentSegment =
5978 | { type : 'text' ; content : string }
6079 | { type : 'thinking' ; content : string }
6180 | { type : 'options' ; data : OptionsTagData }
6281 | { type : 'usage_upgrade' ; data : UsageUpgradeTagData }
6382 | { type : 'credential' ; data : CredentialTagData }
6483 | { type : 'mothership-error' ; data : MothershipErrorTagData }
84+ | { type : 'workspace_resource' ; data : WorkspaceResourceTagData }
6585
6686export type RuntimeSpecialTagName =
6787 | 'thinking'
6888 | 'options'
6989 | 'credential'
7090 | 'mothership-error'
7191 | 'file'
92+ | 'workspace_resource'
7293
7394export interface ParsedSpecialContent {
7495 segments : ContentSegment [ ]
@@ -81,6 +102,7 @@ const RUNTIME_SPECIAL_TAG_NAMES = [
81102 'credential' ,
82103 'mothership-error' ,
83104 'file' ,
105+ 'workspace_resource' ,
84106] as const
85107
86108const SPECIAL_TAG_NAMES = [
@@ -89,6 +111,7 @@ const SPECIAL_TAG_NAMES = [
89111 'usage_upgrade' ,
90112 'credential' ,
91113 'mothership-error' ,
114+ 'workspace_resource' ,
92115] as const
93116
94117function isRecord ( value : unknown ) : value is Record < string , unknown > {
@@ -134,6 +157,16 @@ function isMothershipErrorTagData(value: unknown): value is MothershipErrorTagDa
134157 )
135158}
136159
160+ function isWorkspaceResourceTagData ( value : unknown ) : value is WorkspaceResourceTagData {
161+ if ( ! isRecord ( value ) ) return false
162+ return (
163+ typeof value . type === 'string' &&
164+ ( WORKSPACE_RESOURCE_TAG_TYPES as readonly string [ ] ) . includes ( value . type ) &&
165+ typeof value . id === 'string' &&
166+ value . id . trim ( ) . length > 0
167+ )
168+ }
169+
137170export function parseJsonTagBody < T > (
138171 body : string ,
139172 isExpectedShape : ( value : unknown ) => value is T
@@ -181,6 +214,7 @@ function parseSpecialTagData(
181214 | { type : 'usage_upgrade' ; data : UsageUpgradeTagData }
182215 | { type : 'credential' ; data : CredentialTagData }
183216 | { type : 'mothership-error' ; data : MothershipErrorTagData }
217+ | { type : 'workspace_resource' ; data : WorkspaceResourceTagData }
184218 | null {
185219 if ( tagName === 'thinking' ) {
186220 const content = parseTextTagBody ( body )
@@ -207,11 +241,16 @@ function parseSpecialTagData(
207241 return data ? { type : 'mothership-error' , data } : null
208242 }
209243
244+ if ( tagName === 'workspace_resource' ) {
245+ const data = parseJsonTagBody ( body , isWorkspaceResourceTagData )
246+ return data ? { type : 'workspace_resource' , data } : null
247+ }
248+
210249 return null
211250}
212251
213252/**
214- * Parses inline special tags (`<options>`, `<usage_upgrade>`) from streamed
253+ * Parses inline special tags (`<options>`, `<usage_upgrade>`, `<workspace_resource>` ) from streamed
215254 * text content. Complete tags are extracted into typed segments; incomplete
216255 * tags (still streaming) are suppressed from display and flagged via
217256 * `hasPendingTag` so the caller can show a loading indicator.
@@ -307,12 +346,18 @@ const THINKING_BLOCKS = [
307346interface SpecialTagsProps {
308347 segment : Exclude < ContentSegment , { type : 'text' } >
309348 onOptionSelect ?: ( id : string ) => void
349+ onWorkspaceResourceSelect ?: ( resource : MothershipResource ) => void
310350}
311351
312352/**
313- * Unified renderer for inline special tags: `<options>`, `<usage_upgrade>`, and `<credential>`.
353+ * Unified renderer for inline special tags: `<options>`, `<usage_upgrade>`, `<credential>`,
354+ * and `<workspace_resource>`.
314355 */
315- export function SpecialTags ( { segment, onOptionSelect } : SpecialTagsProps ) {
356+ export function SpecialTags ( {
357+ segment,
358+ onOptionSelect,
359+ onWorkspaceResourceSelect,
360+ } : SpecialTagsProps ) {
316361 switch ( segment . type ) {
317362 case 'thinking' :
318363 return null
@@ -324,6 +369,8 @@ export function SpecialTags({ segment, onOptionSelect }: SpecialTagsProps) {
324369 return < CredentialDisplay data = { segment . data } />
325370 case 'mothership-error' :
326371 return < MothershipErrorDisplay data = { segment . data } />
372+ case 'workspace_resource' :
373+ return < WorkspaceResourceDisplay data = { segment . data } onSelect = { onWorkspaceResourceSelect } />
327374 default :
328375 return null
329376 }
@@ -413,6 +460,102 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) {
413460 )
414461}
415462
463+ function fallbackWorkspaceResourceTitle ( type : WorkspaceResourceTagType ) : string {
464+ switch ( type ) {
465+ case 'workflow' :
466+ return 'Workflow'
467+ case 'table' :
468+ return 'Table'
469+ case 'file' :
470+ return 'File'
471+ }
472+ }
473+
474+ function toMothershipResourceType ( type : WorkspaceResourceTagType ) : MothershipResource [ 'type' ] {
475+ return type
476+ }
477+
478+ function toChatMessageContext ( data : WorkspaceResourceTagData , label : string ) : ChatMessageContext {
479+ switch ( data . type ) {
480+ case 'workflow' :
481+ return { kind : 'workflow' , label, workflowId : data . id }
482+ case 'table' :
483+ return { kind : 'table' , label, tableId : data . id }
484+ case 'file' :
485+ return { kind : 'file' , label, fileId : data . id }
486+ }
487+ }
488+
489+ export function WorkspaceResourceDisplay ( {
490+ data,
491+ onSelect,
492+ } : {
493+ data : WorkspaceResourceTagData
494+ onSelect ?: ( resource : MothershipResource ) => void
495+ } ) {
496+ const { workspaceId } = useParams < { workspaceId : string } > ( )
497+ const { data : workflows = [ ] } = useWorkflows ( workspaceId )
498+ const { data : tables = [ ] } = useTablesList ( workspaceId )
499+ const { data : files = [ ] } = useWorkspaceFiles ( workspaceId )
500+ const { data : knowledgeBases = [ ] } = useKnowledgeBasesQuery ( workspaceId )
501+
502+ const resource = useMemo < MothershipResource > ( ( ) => {
503+ const title =
504+ data . type === 'workflow'
505+ ? ( workflows . find ( ( workflow ) => workflow . id === data . id ) ?. name ??
506+ fallbackWorkspaceResourceTitle ( data . type ) )
507+ : data . type === 'table'
508+ ? ( tables . find ( ( table ) => table . id === data . id ) ?. name ??
509+ fallbackWorkspaceResourceTitle ( data . type ) )
510+ : data . type === 'file'
511+ ? ( files . find ( ( file ) => file . id === data . id ) ?. name ??
512+ fallbackWorkspaceResourceTitle ( data . type ) )
513+ : ( knowledgeBases . find ( ( knowledgeBase ) => knowledgeBase . id === data . id ) ?. name ??
514+ fallbackWorkspaceResourceTitle ( data . type ) )
515+
516+ return {
517+ type : toMothershipResourceType ( data . type ) ,
518+ id : data . id ,
519+ title,
520+ }
521+ } , [ data . id , data . type , files , knowledgeBases , tables , workflows ] )
522+
523+ const context = useMemo ( ( ) => toChatMessageContext ( data , resource . title ) , [ data , resource . title ] )
524+
525+ const workflowColor = useMemo ( ( ) => {
526+ if ( data . type !== 'workflow' ) return null
527+ return workflows . find ( ( workflow ) => workflow . id === data . id ) ?. color ?? null
528+ } , [ data . id , data . type , workflows ] )
529+
530+ const mentionContent = (
531+ < >
532+ < ContextMentionIcon
533+ context = { context }
534+ workflowColor = { workflowColor }
535+ className = 'relative top-0.5 h-[12px] w-[12px] flex-shrink-0 text-[var(--text-icon)]'
536+ />
537+ { resource . title }
538+ </ >
539+ )
540+
541+ const classes =
542+ 'inline-flex items-baseline gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] align-baseline font-[inherit] text-[inherit] leading-[inherit]'
543+
544+ if ( ! onSelect ) {
545+ return < span className = { classes } > { mentionContent } </ span >
546+ }
547+
548+ return (
549+ < button
550+ type = 'button'
551+ onClick = { ( ) => onSelect ( resource ) }
552+ className = { cn ( classes , 'cursor-pointer transition-colors hover-hover:bg-[var(--surface-6)]' ) }
553+ >
554+ { mentionContent }
555+ </ button >
556+ )
557+ }
558+
416559function getCredentialIcon ( provider : string ) : React . ComponentType < { className ?: string } > | null {
417560 const lower = provider . toLowerCase ( )
418561
0 commit comments