@@ -20,13 +20,15 @@ import {
2020} from "@/components/routes/groups/api/groupsApi" ;
2121import {
2222 fetchTextDetails ,
23+ searchSegments ,
2324 searchTitles ,
25+ type SegmentSearchResult ,
2426} from "@/components/api/searchApi" ;
2527import { buildGroupLink , buildSegmentLink } from "@/lib/markdownLinks" ;
2628import { flattenSegments , getLastSegmentId } from "@/lib/utils" ;
2729import pechaIcon from "@/assets/icon/pecha_icon.png" ;
2830
29- type LinkType = "group" | "text" ;
31+ type LinkType = "group" | "text" | "segment" ;
3032
3133export type MarkdownLinkSelection = {
3234 label : string ;
@@ -71,6 +73,8 @@ const MarkdownLinkDialog = ({
7173 const [ selectedSegment , setSelectedSegment ] = useState < SegmentItem | null > (
7274 null ,
7375 ) ;
76+ const [ selectedSearchSegment , setSelectedSearchSegment ] =
77+ useState < SegmentSearchResult | null > ( null ) ;
7478
7579 useEffect ( ( ) => {
7680 if ( ! open ) {
@@ -79,13 +83,15 @@ const MarkdownLinkDialog = ({
7983 setSelectedGroup ( null ) ;
8084 setSelectedTextItem ( null ) ;
8185 setSelectedSegment ( null ) ;
86+ setSelectedSearchSegment ( null ) ;
8287 }
8388 } , [ open ] ) ;
8489
8590 useEffect ( ( ) => {
8691 setSelectedGroup ( null ) ;
8792 setSelectedTextItem ( null ) ;
8893 setSelectedSegment ( null ) ;
94+ setSelectedSearchSegment ( null ) ;
8995 } , [ linkType , debouncedQuery ] ) ;
9096
9197 const { data : groupData , isLoading : isLoadingGroups } = useQuery ( {
@@ -112,7 +118,16 @@ const MarkdownLinkDialog = ({
112118 enabled : open && linkType === "text" && debouncedQuery . length > 0 ,
113119 } ) ;
114120
121+ const { data : segmentSearchData , isLoading : isLoadingSegmentSearch } =
122+ useQuery ( {
123+ queryKey : [ "markdown-link-segment-search" , debouncedQuery ] ,
124+ queryFn : ( ) => searchSegments ( { content : debouncedQuery } ) ,
125+ enabled : open && linkType === "segment" && debouncedQuery . length > 0 ,
126+ } ) ;
127+
115128 const textTitles : TextTitleItem [ ] = titleData ?? [ ] ;
129+ const searchedSegments : SegmentSearchResult [ ] =
130+ segmentSearchData ?. segments ?? [ ] ;
116131
117132 const {
118133 data : detailsData ,
@@ -171,7 +186,9 @@ const MarkdownLinkDialog = ({
171186 const canConfirm =
172187 linkType === "group"
173188 ? ! ! selectedGroup
174- : ! ! selectedTextItem && ! ! selectedSegment ;
189+ : linkType === "segment"
190+ ? ! ! selectedSearchSegment
191+ : ! ! selectedTextItem && ! ! selectedSegment ;
175192
176193 const handleConfirm = ( ) => {
177194 if ( linkType === "group" && selectedGroup ) {
@@ -194,6 +211,17 @@ const MarkdownLinkDialog = ({
194211 url : buildSegmentLink ( segmentId ) ,
195212 } ) ;
196213 onOpenChange ( false ) ;
214+ return ;
215+ }
216+
217+ if ( linkType === "segment" && selectedSearchSegment ) {
218+ const segmentId =
219+ selectedSearchSegment . pecha_segment_id || selectedSearchSegment . id ;
220+ onConfirm ( {
221+ label : selectedText . trim ( ) || selectedSearchSegment . content ,
222+ url : buildSegmentLink ( segmentId ) ,
223+ } ) ;
224+ onOpenChange ( false ) ;
197225 }
198226 } ;
199227
@@ -288,6 +316,58 @@ const MarkdownLinkDialog = ({
288316 ) ;
289317 } ;
290318
319+ const renderDirectSegmentResults = ( ) => {
320+ if ( ! debouncedQuery ) {
321+ return (
322+ < p className = "px-3 py-4 text-sm text-muted-foreground" >
323+ Search for segment content to link directly.
324+ </ p >
325+ ) ;
326+ }
327+
328+ if ( isLoadingSegmentSearch ) {
329+ return (
330+ < p className = "px-3 py-4 text-sm text-muted-foreground" >
331+ Searching segments…
332+ </ p >
333+ ) ;
334+ }
335+
336+ if ( searchedSegments . length === 0 ) {
337+ return (
338+ < p className = "px-3 py-4 text-sm text-muted-foreground" >
339+ No segments found.
340+ </ p >
341+ ) ;
342+ }
343+
344+ return (
345+ < ul >
346+ { searchedSegments . map ( ( segment ) => {
347+ const isSelected = selectedSearchSegment ?. id === segment . id ;
348+ return (
349+ < li key = { segment . id } >
350+ < button
351+ type = "button"
352+ className = { `w-full rounded-md border p-2 text-left text-sm hover:bg-muted/40 ${
353+ isSelected
354+ ? "border-[#801A1E] bg-muted/40"
355+ : "border-dashed border-gray-300 dark:border-[#313132]"
356+ } `}
357+ onClick = { ( ) => setSelectedSearchSegment ( segment ) }
358+ >
359+ < div
360+ className = "line-clamp-3"
361+ dangerouslySetInnerHTML = { { __html : segment . content } }
362+ />
363+ </ button >
364+ </ li >
365+ ) ;
366+ } ) }
367+ </ ul >
368+ ) ;
369+ } ;
370+
291371 const renderSegmentResults = ( ) => {
292372 if ( ! selectedTextItem ) return null ;
293373
@@ -370,13 +450,28 @@ const MarkdownLinkDialog = ({
370450 >
371451 Text
372452 </ button >
453+ < button
454+ type = "button"
455+ className = { `flex-1 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
456+ linkType === "segment"
457+ ? "bg-muted text-foreground"
458+ : "text-muted-foreground hover:text-foreground"
459+ } `}
460+ onClick = { ( ) => setLinkType ( "segment" ) }
461+ >
462+ Segment
463+ </ button >
373464 </ div >
374465
375466 < div className = "flex items-center rounded-md border border-input px-2" >
376467 < IoMdSearch className = "size-4 shrink-0 text-muted-foreground" />
377468 < Pecha . Input
378469 placeholder = {
379- linkType === "group" ? "Search groups…" : "Search text titles…"
470+ linkType === "group"
471+ ? "Search groups…"
472+ : linkType === "text"
473+ ? "Search text titles…"
474+ : "Search segment content…"
380475 }
381476 className = "border-0 shadow-none focus-visible:ring-0"
382477 value = { searchQuery }
@@ -385,7 +480,11 @@ const MarkdownLinkDialog = ({
385480 </ div >
386481
387482 < div className = "max-h-52 overflow-y-auto rounded-md border border-input" >
388- { linkType === "group" ? renderGroupResults ( ) : renderTextResults ( ) }
483+ { linkType === "group"
484+ ? renderGroupResults ( )
485+ : linkType === "text"
486+ ? renderTextResults ( )
487+ : renderDirectSegmentResults ( ) }
389488 </ div >
390489
391490 { linkType === "text" && renderSegmentResults ( ) }
0 commit comments