1
+ import type { SelectTriggerProps } from "@radix-ui/react-select" ;
1
2
import { API } from "api/api" ;
2
3
import { getErrorDetail , getErrorMessage } from "api/errors" ;
3
4
import { templateVersionPresets } from "api/queries/templates" ;
@@ -29,15 +30,16 @@ import {
29
30
} from "components/Tooltip/Tooltip" ;
30
31
import { useAuthenticated } from "hooks/useAuthenticated" ;
31
32
import { useExternalAuth } from "hooks/useExternalAuth" ;
32
- import { RedoIcon , RotateCcwIcon , SendIcon } from "lucide-react" ;
33
+ import { ArrowUpIcon , RedoIcon , RotateCcwIcon } from "lucide-react" ;
33
34
import { AI_PROMPT_PARAMETER_NAME } from "modules/tasks/tasks" ;
34
35
import { type FC , useEffect , useState } from "react" ;
35
36
import { useMutation , useQuery , useQueryClient } from "react-query" ;
36
- import TextareaAutosize from "react-textarea-autosize" ;
37
+ import TextareaAutosize , {
38
+ type TextareaAutosizeProps ,
39
+ } from "react-textarea-autosize" ;
40
+ import { cn } from "utils/cn" ;
37
41
import { docs } from "utils/docs" ;
38
42
39
- const textareaPlaceholder = "Prompt your AI agent to start a task..." ;
40
-
41
43
type TaskPromptProps = {
42
44
templates :Template [ ] | undefined ;
43
45
error :unknown ;
@@ -92,23 +94,14 @@ const TaskPromptLoadingError: FC<{
92
94
93
95
const TaskPromptSkeleton :FC = ( ) => {
94
96
return (
95
- < div className = "border border-border border-solid rounded-lg p-4" >
96
- < div className = "space-y-4" >
97
- { /* Textarea skeleton */ }
98
- < TextareaAutosize
99
- disabled
100
- id = "prompt"
101
- name = "prompt"
102
- placeholder = { textareaPlaceholder }
103
- className = { `border-0 resize-none w-full h-full bg-transparent rounded-lg outline-none flex min-h-[60px]
104
- text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm` }
105
- />
97
+ < div className = "border border-border border-solid rounded-3xl p-3 bg-surface-secondary" >
98
+ { /* Textarea skeleton */ }
99
+ < PromptTextarea disabled />
106
100
107
- { /* Bottom controls skeleton */ }
108
- < div className = "flex items-center justify-between pt-2" >
109
- < Skeleton className = "w-[208px] h-8" />
110
- < Skeleton className = "w-[96px] h-8" />
111
- </ div >
101
+ { /* Bottom controls skeleton */ }
102
+ < div className = "flex items-center justify-between pt-2" >
103
+ < Skeleton className = "w-[208px] h-8 rounded-full" />
104
+ < Skeleton className = "size-8 rounded-full" />
112
105
</ div >
113
106
</ div >
114
107
) ;
@@ -225,7 +218,7 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
225
218
{ externalAuthError && < ErrorAlert error = { externalAuthError } /> }
226
219
227
220
< fieldset
228
- className = "border border-border border-solid rounded-lg p-4 "
221
+ className = "border border-border border-solid rounded-3xl p-3 bg-surface-secondary "
229
222
disabled = { createTaskMutation . isPending }
230
223
>
231
224
< label
@@ -238,25 +231,15 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
238
231
>
239
232
{ isPromptReadOnly ?"Prompt defined by preset" :"Prompt" }
240
233
</ label >
241
- < TextareaAutosize
234
+ < PromptTextarea
242
235
required
243
- id = "prompt"
244
- name = "prompt"
245
236
value = { presetAIPrompt || undefined }
246
237
readOnly = { isPromptReadOnly }
247
- placeholder = { textareaPlaceholder }
248
- className = { `border-0 resize-none w-full h-full bg-transparent rounded-lg outline-none flex min-h-[60px]
249
- text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm${
250
- isPromptReadOnly ?"opacity-60 cursor-not-allowed" :""
251
- } `}
252
238
/>
253
239
< div className = "flex items-center justify-between pt-2" >
254
- < div className = "flex items-center gap-4 " >
240
+ < div className = "flex items-center gap-1 " >
255
241
< div className = "flex flex-col gap-1" >
256
- < label
257
- htmlFor = "templateID"
258
- className = "text-xs font-medium text-content-primary"
259
- >
242
+ < label htmlFor = "templateID" className = "sr-only" >
260
243
Template
261
244
</ label >
262
245
< Select
@@ -265,12 +248,9 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
265
248
defaultValue = { templates [ 0 ] . id }
266
249
required
267
250
>
268
- < SelectTrigger
269
- id = "templateID"
270
- className = "w-80 text-xs [&_svg]:size-icon-xs border-0 bg-surface-secondary h-8 px-3"
271
- >
251
+ < PromptSelectTrigger id = "templateID" >
272
252
< SelectValue placeholder = "Select a template" />
273
- </ SelectTrigger >
253
+ </ PromptSelectTrigger >
274
254
< SelectContent >
275
255
{ templates . map ( ( template ) => {
276
256
return (
@@ -285,40 +265,26 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
285
265
</ Select >
286
266
</ div >
287
267
288
- { isLoadingPresets ?(
289
- < div className = "flex flex-col gap-1" >
290
- < label
291
- htmlFor = "presetID"
292
- className = "text-xs font-medium text-content-primary"
293
- >
294
- Preset
295
- </ label >
268
+ < div className = "flex flex-col gap-1" >
269
+ < label htmlFor = "presetID" className = "sr-only" >
270
+ Preset
271
+ </ label >
272
+ { isLoadingPresets ?(
296
273
< Skeleton className = "w-[320px] h-8" />
297
- </ div >
298
- ) :(
299
- presets &&
300
- presets . length > 0 && (
301
- < div className = "flex flex-col gap-1" >
302
- < label
303
- htmlFor = "presetID"
304
- className = "text-xs font-medium text-content-primary"
305
- >
306
- Preset
307
- </ label >
274
+ ) :(
275
+ presets &&
276
+ presets . length > 0 && (
308
277
< Select
309
278
key = { `preset-select-${ selectedTemplate . active_version_id } ` }
310
279
name = "presetID"
311
280
value = { selectedPresetId || undefined }
312
281
onValueChange = { setSelectedPresetId }
313
282
>
314
- < SelectTrigger
315
- id = "presetID"
316
- className = "w-80 text-xs [&_svg]:size-icon-xs border-0 bg-surface-secondary h-8 px-3"
317
- >
283
+ < PromptSelectTrigger id = "presetID" >
318
284
< SelectValue placeholder = "Select a preset" />
319
- </ SelectTrigger >
285
+ </ PromptSelectTrigger >
320
286
< SelectContent >
321
- { presets . toSorted ( sortByDefault ) . map ( ( preset ) => (
287
+ { presets ? .toSorted ( sortByDefault ) . map ( ( preset ) => (
322
288
< SelectItem value = { preset . ID } key = { preset . ID } >
323
289
< span className = "overflow-hidden text-ellipsis block" >
324
290
{ preset . Name } { preset . Default && "(Default)" }
@@ -327,9 +293,9 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
327
293
) ) }
328
294
</ SelectContent >
329
295
</ Select >
330
- </ div >
331
- )
332
- ) }
296
+ )
297
+ ) }
298
+ </ div >
333
299
</ div >
334
300
335
301
< div className = "flex items-center gap-2" >
@@ -340,17 +306,22 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
340
306
/>
341
307
) }
342
308
343
- < Button size = "sm" type = "submit" disabled = { isMissingExternalAuth } >
309
+ < Button
310
+ size = "icon"
311
+ type = "submit"
312
+ disabled = { isMissingExternalAuth }
313
+ className = "rounded-full disabled:bg-surface-invert-primary disabled:opacity-70"
314
+ >
344
315
< Spinner
345
316
loading = {
346
317
isLoadingExternalAuth ||
347
318
isPollingExternalAuth ||
348
319
createTaskMutation . isPending
349
320
}
350
321
>
351
- < SendIcon />
322
+ < ArrowUpIcon />
352
323
</ Spinner >
353
- Run task
324
+ < span className = "sr-only" > Run task</ span >
354
325
</ Button >
355
326
</ div >
356
327
</ div >
@@ -359,6 +330,23 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
359
330
) ;
360
331
} ;
361
332
333
+ const PromptSelectTrigger :FC < SelectTriggerProps > = ( {
334
+ className,
335
+ ...props
336
+ } ) => {
337
+ return (
338
+ < SelectTrigger
339
+ { ...props }
340
+ className = { cn ( [
341
+ className ,
342
+ `border-0 bg-surface-secondary text-sm text-content-primary gap-2 px-3
343
+ [&_svg]:text-inherit cursor-pointer hover:bg-surface-quaternary rounded-full
344
+ h-8 data-[state=open]:bg-surface-tertiary` ,
345
+ ] ) }
346
+ />
347
+ ) ;
348
+ } ;
349
+
362
350
type ExternalAuthButtonProps = {
363
351
template :Template ;
364
352
missedExternalAuth :TemplateVersionExternalAuth [ ] ;
@@ -379,7 +367,7 @@ const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
379
367
return (
380
368
< div className = "flex items-center gap-2" key = { auth . id } >
381
369
< Button
382
- variant = "outline "
370
+ className = "bg-surface-tertiary hover:bg-surface-quaternary rounded-full text-white "
383
371
size = "sm"
384
372
disabled = { isPollingExternalAuth || auth . authenticated }
385
373
onClick = { ( ) => {
@@ -446,3 +434,17 @@ async function createTaskWithLatestTemplateVersion(
446
434
template_version_preset_id :presetId ,
447
435
} ) ;
448
436
}
437
+
438
+ const PromptTextarea :FC < TextareaAutosizeProps > = ( props ) => {
439
+ return (
440
+ < TextareaAutosize
441
+ required
442
+ id = "prompt"
443
+ name = "prompt"
444
+ placeholder = "Prompt your AI agent to start a task..."
445
+ className = { `border-0 px-3 py-2 resize-none w-full h-full bg-transparent rounded-lg
446
+ outline-none flex min-h-24 text-sm shadow-sm text-content-primary
447
+ placeholder:text-content-secondary md:text-sm${ props . readOnly ?"opacity-60 cursor-not-allowed" :"" } ` }
448
+ />
449
+ ) ;
450
+ } ;