11'use client'
22
3- import { useEffect , useRef , useState } from 'react'
3+ import { useEffect , useRef } from 'react'
44import {
55 Button ,
66 Chip ,
@@ -13,6 +13,7 @@ import {
1313 toast ,
1414} from '@sim/emcn'
1515import { ArrowUp , Paperclip , Plus , Slash } from '@sim/emcn/icons'
16+ import { useQueries } from '@tanstack/react-query'
1617import {
1718 ASSISTANT_IMAGE_ACCEPT_ATTRIBUTE ,
1819 isAssistantImageType ,
@@ -21,19 +22,22 @@ import { MOTHERSHIP_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
2122import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
2223import { AttachedFilesList } from '@/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list'
2324import { DropOverlay } from '@/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay/drop-overlay'
25+ import { FastModeToggle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/fast-mode-toggle'
2426import { InputToolbar } from '@/app/workspace/[workspaceId]/home/components/user-input/components/input-toolbar'
2527import { MicButton } from '@/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button'
2628import { MicrophonePermissionHelp } from '@/app/workspace/[workspaceId]/home/components/user-input/components/microphone-permission-help/microphone-permission-help'
2729import {
2830 PromptEditor ,
2931 usePromptEditor ,
3032} from '@/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor'
33+ import { organizationSkillOptions } from '@/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/organization-skill-options'
3134import type { ChatRequestMode } from '@/app/workspace/[workspaceId]/home/types'
3235import type { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
3336import {
3437 escapeRegex ,
3538 SKILL_CHIP_TRIGGER ,
3639} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
40+ import { getSkillsQueryOptions } from '@/hooks/queries/skills'
3741import { useWorkspacesQuery } from '@/hooks/queries/workspace'
3842import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
3943import { useChatInputFocus } from '@/hooks/use-chat-input-focus'
@@ -47,9 +51,10 @@ const SEND_BUTTON_DISABLED = 'bg-[#808080] dark:bg-[#808080]'
4751
4852interface ComposerProps {
4953 requestMode ?: ChatRequestMode
54+ assistantFast ?: boolean
55+ onAssistantFastChange ?: ( enabled : boolean ) => void
5056 onModeChange ?: ( mode : ChatRequestMode ) => void
5157 showModeSelector ?: boolean
52- modeChangeDisabled ?: boolean
5358 value : string
5459 files : ReturnType < typeof useFileAttachments >
5560 /** On the empty home the placeholder types itself and the field is taller; in a chat it is the plain footer input. */
@@ -69,8 +74,9 @@ interface ComposerProps {
6974export function Composer ( {
7075 requestMode = 'assistant' ,
7176 onModeChange,
77+ assistantFast = false ,
78+ onAssistantFastChange,
7279 showModeSelector = false ,
73- modeChangeDisabled = false ,
7480 value,
7581 files,
7682 isInitialView,
@@ -82,17 +88,24 @@ export function Composer({
8288} : ComposerProps ) {
8389 const imagesOnly = requestMode === 'assistant'
8490 const { organization } = useOrganizationContext ( )
85- const [ contextWorkspaceId , setContextWorkspaceId ] = useState ( '' )
86- const [ pendingPicker , setPendingPicker ] = useState < {
87- kind : 'resources' | 'skills'
88- anchor : { left : number ; top : number }
89- } | null > ( null )
9091 const { data : allWorkspaces = [ ] } = useWorkspacesQuery ( ! imagesOnly )
91- const workspaces = allWorkspaces . filter (
92+ const workspaces = ( imagesOnly ? [ ] : allWorkspaces ) . filter (
9293 ( workspace ) => workspace . organizationId === organization . id
9394 )
95+ const skillQueries = useQueries ( {
96+ queries : workspaces . map ( ( workspace ) => getSkillsQueryOptions ( workspace . id ) ) ,
97+ } )
98+ const skills = imagesOnly
99+ ? [ ]
100+ : organizationSkillOptions (
101+ workspaces . map ( ( workspace , index ) => ( {
102+ ...workspace ,
103+ skills : skillQueries [ index ] . isPlaceholderData ? [ ] : ( skillQueries [ index ] . data ?? [ ] ) ,
104+ } ) )
105+ )
94106 const editor = usePromptEditor ( {
95- workspaceId : contextWorkspaceId ,
107+ workspaceId : '' ,
108+ availableSkills : skills ,
96109 organizationId : organization . id ,
97110 contextsEnabled : ! imagesOnly ,
98111 initialValue : value ,
@@ -132,12 +145,6 @@ export function Composer({
132145 editorRef . current . setValue ( restoredText , { chipify : false } )
133146 editorRef . current . setContexts ( restoredContexts )
134147 } , [ restoredContexts ] )
135- useEffect ( ( ) => {
136- if ( ! pendingPicker ) return
137- if ( pendingPicker . kind === 'resources' ) editorRef . current . openResourceMenu ( pendingPicker . anchor )
138- else editorRef . current . insertSlashTrigger ( )
139- setPendingPicker ( null )
140- } , [ pendingPicker ] )
141148 useChatInputFocus ( { textareaRef } )
142149 const voice = useVoiceInput ( {
143150 organizationId : organization . id ,
@@ -163,34 +170,23 @@ export function Composer({
163170 }
164171
165172 const contextPicker = ( kind : 'resources' | 'skills' , icon : typeof Plus , label : string ) => (
166- < DropdownMenu >
167- < Tooltip . Root >
168- < Tooltip . Trigger asChild >
169- < DropdownMenuTrigger asChild >
170- < Chip shape = 'round' leftIcon = { icon } aria-label = { label } />
171- </ DropdownMenuTrigger >
172- </ Tooltip . Trigger >
173- < Tooltip . Content side = 'top' > { label } </ Tooltip . Content >
174- </ Tooltip . Root >
175- < DropdownMenuContent side = 'top' align = 'start' >
176- < div className = 'px-2 py-1 text-[var(--text-muted)] text-xs' > Choose a workspace</ div >
177- { workspaces . map ( ( workspace ) => (
178- < DropdownMenuItem
179- key = { workspace . id }
180- onSelect = { ( event ) => {
181- const rect = ( event . currentTarget as HTMLElement ) . getBoundingClientRect ( )
182- setContextWorkspaceId ( workspace . id )
183- setPendingPicker ( { kind, anchor : { left : rect . left , top : rect . top } } )
184- } }
185- >
186- { workspace . name }
187- </ DropdownMenuItem >
188- ) ) }
189- { ! workspaces . length && (
190- < DropdownMenuItem disabled > No accessible workspaces</ DropdownMenuItem >
191- ) }
192- </ DropdownMenuContent >
193- </ DropdownMenu >
173+ < Tooltip . Root >
174+ < Tooltip . Trigger asChild >
175+ < Chip
176+ shape = 'round'
177+ leftIcon = { icon }
178+ aria-label = { label }
179+ onClick = { ( event ) => {
180+ if ( kind === 'skills' ) editor . insertSlashTrigger ( )
181+ else {
182+ const rect = event . currentTarget . getBoundingClientRect ( )
183+ editor . openResourceMenu ( { left : rect . left , top : rect . top } )
184+ }
185+ } }
186+ />
187+ </ Tooltip . Trigger >
188+ < Tooltip . Content side = 'top' > { label } </ Tooltip . Content >
189+ </ Tooltip . Root >
194190 )
195191
196192 return (
@@ -240,10 +236,7 @@ export function Composer({
240236 { showModeSelector && (
241237 < DropdownMenu >
242238 < DropdownMenuTrigger asChild >
243- < Chip
244- aria-label = 'Conversation mode'
245- disabled = { isSending || modeChangeDisabled || ! onModeChange }
246- >
239+ < Chip aria-label = 'Conversation mode' disabled = { ! onModeChange } >
247240 { requestMode === 'assistant' ? 'Search' : 'Build' }
248241 </ Chip >
249242 </ DropdownMenuTrigger >
@@ -253,9 +246,8 @@ export function Composer({
253246 key = { mode }
254247 role = 'menuitemradio'
255248 aria-checked = { mode === requestMode }
256- disabled = { isSending || modeChangeDisabled }
257249 onSelect = { ( ) => {
258- if ( isSending || modeChangeDisabled || mode === requestMode ) return
250+ if ( mode === requestMode ) return
259251 if (
260252 mode === 'assistant' &&
261253 ( editor . getActiveContexts ( ) . length > 0 ||
@@ -286,6 +278,17 @@ export function Composer({
286278 />
287279 )
288280 }
281+ beforeSubmitControl = {
282+ imagesOnly &&
283+ onAssistantFastChange && (
284+ < FastModeToggle
285+ enabled = { assistantFast }
286+ onChange = { onAssistantFastChange }
287+ label = 'Search Fast mode'
288+ description = 'Faster and cheaper'
289+ />
290+ )
291+ }
289292 submitControl = {
290293 isSending ? (
291294 < Button
0 commit comments