@@ -3,6 +3,7 @@ import { fetchClientWithThrow } from "@/lib/api/api";
3
3
import { cn } from "@/lib/utils" ;
4
4
import { ErrorComponent , Link , createFileRoute } from "@tanstack/react-router" ;
5
5
import { BotIcon } from "lucide-react" ;
6
+ import { useState , useRef , useEffect } from "react" ;
6
7
7
8
export const Route = createFileRoute ( "/_chat/_layout/" ) ( {
8
9
loader : async ( ) => {
@@ -26,60 +27,106 @@ function Index() {
26
27
< div className = "flex flex-1 flex-col gap-4 p-4 justify-center" >
27
28
< div className = "divide-y divide-border overflow-hidden rounded-lg shadow sm:grid sm:grid-cols-2 sm:gap-1 sm:divide-y-0" >
28
29
{ agents . map ( ( agent , agentIdx ) => (
29
- < div
30
+ < AgentCard
30
31
key = { agent . id }
32
+ agent = { agent }
33
+ agentIdx = { agentIdx }
34
+ agents = { agents }
35
+ />
36
+ ) ) }
37
+ </ div >
38
+ </ div >
39
+ </ div >
40
+ ) ;
41
+ }
42
+
43
+ function AgentCard ( {
44
+ agent,
45
+ agentIdx,
46
+ agents,
47
+ } : {
48
+ agent : any ;
49
+ agentIdx : number ;
50
+ agents : any [ ] ;
51
+ } ) {
52
+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
53
+ const [ shouldTruncate , setShouldTruncate ] = useState ( false ) ;
54
+ const textRef = useRef < HTMLParagraphElement > ( null ) ;
55
+
56
+ useEffect ( ( ) => {
57
+ if ( textRef . current && agent . description ) {
58
+ // Check if the text is actually being truncated by comparing scroll height vs client height
59
+ const element = textRef . current ;
60
+ setShouldTruncate ( element . scrollHeight > element . clientHeight ) ;
61
+ }
62
+ } , [ agent . description ] ) ;
63
+
64
+ return (
65
+ < div
66
+ className = { cn (
67
+ agentIdx === 0 ? "rounded-tl-lg rounded-tr-lg sm:rounded-tr-none" : "" ,
68
+ agentIdx === 1 ? "sm:rounded-tr-lg" : "" ,
69
+ agentIdx === agents . length - 2 ? "sm:rounded-bl-lg" : "" ,
70
+ agentIdx === agents . length - 1
71
+ ? "rounded-bl-lg rounded-br-lg sm:rounded-bl-none"
72
+ : "" ,
73
+ "group relative bg-muted/40 p-6 focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring"
74
+ ) }
75
+ >
76
+ < div >
77
+ < span
78
+ className = { cn ( "inline-flex rounded-lg p-3 ring-4 ring-secondary" ) }
79
+ >
80
+ < BotIcon aria-hidden = "true" className = "size-6" />
81
+ </ span >
82
+ </ div >
83
+ < div className = "mt-8" >
84
+ < h3 className = "text-base font-semibold" >
85
+ < Link
86
+ to = "/agent/$agentId"
87
+ params = { { agentId : agent . id } }
88
+ className = "focus:outline-none"
89
+ >
90
+ { /* Extend touch target to entire panel */ }
91
+ < span aria-hidden = "true" className = "absolute inset-0" />
92
+ { agent . name }
93
+ </ Link >
94
+ </ h3 >
95
+ { agent . description && (
96
+ < div className = "mt-2" >
97
+ < p
98
+ ref = { textRef }
31
99
className = { cn (
32
- agentIdx === 0
33
- ? "rounded-tl-lg rounded-tr-lg sm:rounded-tr-none"
34
- : "" ,
35
- agentIdx === 1 ? "sm:rounded-tr-lg" : "" ,
36
- agentIdx === agents . length - 2 ? "sm:rounded-bl-lg" : "" ,
37
- agentIdx === agents . length - 1
38
- ? "rounded-bl-lg rounded-br-lg sm:rounded-bl-none"
39
- : "" ,
40
- "group relative bg-muted/40 p-6 focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring" ,
100
+ "text-sm text-muted-foreground" ,
101
+ ! isExpanded && "line-clamp-2"
41
102
) }
42
103
>
43
- < div >
44
- < span
45
- className = { cn (
46
- "inline-flex rounded-lg p-3 ring-4 ring-secondary" ,
47
- ) }
48
- >
49
- < BotIcon aria-hidden = "true" className = "size-6" />
50
- </ span >
51
- </ div >
52
- < div className = "mt-8" >
53
- < h3 className = "text-base font-semibold" >
54
- < Link
55
- to = "/agent/$agentId"
56
- params = { { agentId : agent . id } }
57
- className = "focus:outline-none"
58
- >
59
- { /* Extend touch target to entire panel */ }
60
- < span aria-hidden = "true" className = "absolute inset-0" />
61
- { agent . name }
62
- </ Link >
63
- </ h3 >
64
- { /* may put here agent's description
65
- <p className="mt-2 text-sm text-gray-500">
66
- ...
67
- </p>
68
- */ }
69
- </ div >
70
- < span
71
- aria-hidden = "true"
72
- className = "pointer-events-none absolute right-6 top-6 text-muted-foreground/20 group-hover:text-muted-foreground/40"
104
+ { agent . description }
105
+ </ p >
106
+ { shouldTruncate && (
107
+ < button
108
+ onClick = { ( e ) => {
109
+ e . preventDefault ( ) ;
110
+ e . stopPropagation ( ) ;
111
+ setIsExpanded ( ! isExpanded ) ;
112
+ } }
113
+ className = "relative z-10 mt-1 text-xs text-primary hover:text-primary/80 focus:outline-none rounded pointer-events-auto"
73
114
>
74
- < svg fill = "currentColor" viewBox = "0 0 24 24" className = "size-6" >
75
- < title > Open</ title >
76
- < path d = "M20 4h1a1 1 0 00-1-1v1zm-1 12a1 1 0 102 0h-2zM8 3a1 1 0 000 2V3zM3.293 19.293a1 1 0 101.414 1.414l-1.414-1.414zM19 4v12h2V4h-2zm1-1H8v2h12V3zm-.707.293l-16 16 1.414 1.414 16-16-1.414-1.414z" />
77
- </ svg >
78
- </ span >
79
- </ div >
80
- ) ) }
81
- </ div >
115
+ { isExpanded ? "Show less" : "Show more" }
116
+ </ button >
117
+ ) }
118
+ </ div >
119
+ ) }
82
120
</ div >
121
+ < span
122
+ aria-hidden = "true"
123
+ className = "pointer-events-none absolute right-6 top-6 text-muted-foreground/20 group-hover:text-muted-foreground/40"
124
+ >
125
+ < svg fill = "currentColor" viewBox = "0 0 24 24" className = "size-6" >
126
+ < title > Open</ title >
127
+ < path d = "M20 4h1a1 1 0 00-1-1v1zm-1 12a1 1 0 102 0h-2zM8 3a1 1 0 000 2V3zM3.293 19.293a1 1 0 101.414 1.414l-1.414-1.414zM19 4v12h2V4h-2zm1-1H8v2h12V3zm-.707.293l-16 16 1.414 1.414 16-16-1.414-1.414z" />
128
+ </ svg >
129
+ </ span >
83
130
</ div >
84
131
) ;
85
132
}
0 commit comments