@@ -5,6 +5,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
55import { twMerge } from "tailwind-merge" ;
66import PublishNew from "./icons/publish-new.svg" ;
77import QuickStop from "./icons/quick-stop.svg" ;
8+ import type { OnTextInject , TriggerConfig } from "./suggestion" ;
9+ import Suggestion from "./suggestion" ;
810import type { Backend } from "./utils" ;
911
1012export interface InputCountProps extends React . ComponentProps < "span" > {
@@ -105,6 +107,10 @@ export interface SenderProps extends React.ComponentProps<"div"> {
105107 */
106108 onSend ?: ( controller : AbortController ) => void ;
107109 toolbar ?: React . ReactNode ;
110+ /**
111+ * Trigger configurations for the suggestion list
112+ */
113+ triggerConfigs ?: TriggerConfig [ ] ;
108114}
109115
110116export function Sender ( {
@@ -115,11 +121,13 @@ export function Sender({
115121 input,
116122 onSend,
117123 toolbar,
124+ triggerConfigs,
118125 ...props
119126} : SenderProps ) {
120127 const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
121128 const [ message , setMessage ] = useState ( initialMessage ) ;
122129 const [ isSending , setIsSending ] = useState ( false ) ;
130+ const [ caretPosition , setCaretPosition ] = useState < number | null > ( null ) ;
123131
124132 useEffect ( ( ) => {
125133 if ( textareaRef . current ) {
@@ -129,6 +137,15 @@ export function Sender({
129137 onMessageChange ?.( message ) ;
130138 } , [ message , onMessageChange ] ) ;
131139
140+ useEffect ( ( ) => {
141+ if ( textareaRef . current && caretPosition !== null ) {
142+ textareaRef . current . focus ( ) ;
143+ textareaRef . current . selectionStart = caretPosition ;
144+ textareaRef . current . selectionEnd = caretPosition ;
145+ setCaretPosition ( null ) ;
146+ }
147+ } , [ caretPosition ] ) ;
148+
132149 const [ controller , setController ] = useState < AbortController | null > ( null ) ;
133150 const handleSend = useCallback ( ( ) => {
134151 if ( isSending ) {
@@ -175,19 +192,41 @@ export function Sender({
175192 [ ] ,
176193 ) ;
177194
195+ const handleTextInject : OnTextInject = useCallback (
196+ ( newText , suggestionStartPosition ) => {
197+ setMessage ( ( prevMessage ) => {
198+ const currentCaretPosition =
199+ textareaRef . current ?. selectionStart ?? prevMessage . length ;
200+ const textBefore = prevMessage . slice ( 0 , suggestionStartPosition ) ;
201+ const textAfter = prevMessage . slice ( currentCaretPosition ) ;
202+ const newMessage = textBefore + newText + textAfter ;
203+ return newMessage ;
204+ } ) ;
205+ setCaretPosition ( suggestionStartPosition + newText . length ) ;
206+ } ,
207+ [ ] ,
208+ ) ;
178209 return (
179210 < div
180211 data-slot = "sender"
181212 className = { twMerge (
182213 clsx (
183- "px-1 flex flex-col items-center border rounded-2xl" ,
214+ "relative px-1 flex flex-col items-center border rounded-2xl" ,
184215 "border-gray-200 dark:border-gray-700 shadow-sm transition-all duration-300 hover:shadow-md" ,
185216 "focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500" ,
186217 className ,
187218 ) ,
188219 ) }
189220 { ...props }
190221 >
222+ < div className = "absolute bottom-full left-0 w-full bg-white dark:bg-gray-50 rounded-lg shadow-amber-50 max-h-64 overflow-y-auto" >
223+ < Suggestion
224+ message = { message }
225+ textareaRef = { textareaRef }
226+ triggerConfigs = { triggerConfigs ?? [ ] }
227+ onInject = { handleTextInject }
228+ />
229+ </ div >
191230 < textarea
192231 ref = { textareaRef }
193232 value = { message }
0 commit comments