Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,48 @@ const useAttachmentToolController = ({ launcher, couldAddImageFile, extensions,
const { t } = useTranslation()
const [selecting, setSelecting] = useState<boolean>(false)

const openFileSelectDialog = useCallback(async () => {
if (selecting) {
return
}
// when the number of extensions is greater than 20, use *.* to avoid selecting window lag
const useAllFiles = extensions.length > 20

setSelecting(true)
const _files = await window.api.file.select({
properties: ['openFile', 'multiSelections'],
filters: [
{
name: 'Files',
extensions: useAllFiles ? ['*'] : extensions.map((i) => i.replace('.', ''))
}
]
})
setSelecting(false)

if (_files) {
if (!useAllFiles) {
setFiles((currentFiles) => [...currentFiles, ...toComposerAttachments(_files)])
const openFileSelectDialog = useCallback(
async (restoreFocus?: () => void) => {
if (selecting) {
return
}
const supportedFiles = await filterSupportedFiles(_files, extensions)
if (supportedFiles.length > 0) {
setFiles((currentFiles) => [...currentFiles, ...toComposerAttachments(supportedFiles)])
}
// when the number of extensions is greater than 20, use *.* to avoid selecting window lag
const useAllFiles = extensions.length > 20

setSelecting(true)
const _files = await window.api.file.select({
properties: ['openFile', 'multiSelections'],
filters: [
{
name: 'Files',
extensions: useAllFiles ? ['*'] : extensions.map((i) => i.replace('.', ''))
}
]
})
setSelecting(false)
window.requestAnimationFrame(() => restoreFocus?.())

if (supportedFiles.length !== _files.length) {
window.toast.info(
t('chat.input.file_not_supported_count', {
count: _files.length - supportedFiles.length
})
)
if (_files) {
if (!useAllFiles) {
setFiles((currentFiles) => [...currentFiles, ...toComposerAttachments(_files)])
return
}
const supportedFiles = await filterSupportedFiles(_files, extensions)
if (supportedFiles.length > 0) {
setFiles((currentFiles) => [...currentFiles, ...toComposerAttachments(supportedFiles)])
}

if (supportedFiles.length !== _files.length) {
window.toast.info(
t('chat.input.file_not_supported_count', {
count: _files.length - supportedFiles.length
})
)
}
}
}
}, [extensions, selecting, setFiles, t])
},
[extensions, selecting, setFiles, t]
)

useEffect(() => {
const isDocumentOnly = !couldAddImageFile
Expand All @@ -72,8 +76,8 @@ const useAttachmentToolController = ({ launcher, couldAddImageFile, extensions,
icon: <Paperclip />,
suffix: isDocumentOnly ? t('chat.input.upload.document_only') : undefined,
disabled,
action: () => {
void openFileSelectDialog()
action: ({ inputAdapter }) => {
void openFileSelectDialog(inputAdapter?.focus)
}
}
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const logger = loggerService.withContext('QuickPhrasesButton')
const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [isManageModalOpen, setIsManageModalOpen] = useState(false)
const restoreInputFocusRef = useRef<(() => void) | null>(null)
const { t } = useTranslation()
const {
isVisible: isQuickPanelVisible,
Expand Down Expand Up @@ -76,6 +77,13 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
[insertText]
)

const restoreInputFocus = useCallback(() => {
window.requestAnimationFrame(() => {
restoreInputFocusRef.current?.()
restoreInputFocusRef.current = null
})
}, [])

const handleAddModalSave = useCallback(
async (data: { title: string; content: string }) => {
try {
Expand All @@ -86,11 +94,37 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
}
})
setIsAddModalOpen(false)
restoreInputFocus()
} catch {
// handled by useMutation onError
}
},
[createPrompt]
[createPrompt, restoreInputFocus]
)

const openAddModal = useCallback((options?: QuickPanelCallBackOptions) => {
restoreInputFocusRef.current = options?.inputAdapter?.focus ?? null
setIsAddModalOpen(true)
}, [])

const closeAddModal = useCallback(() => {
setIsAddModalOpen(false)
restoreInputFocus()
}, [restoreInputFocus])

const openManageModal = useCallback((options?: QuickPanelCallBackOptions) => {
restoreInputFocusRef.current = options?.inputAdapter?.focus ?? null
setIsManageModalOpen(true)
}, [])

const handleManageModalOpenChange = useCallback(
(open: boolean) => {
setIsManageModalOpen(open)
if (!open) {
restoreInputFocus()
}
},
[restoreInputFocus]
)

const phraseItems = useMemo(() => {
Expand Down Expand Up @@ -122,17 +156,17 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
newList.push({
label: t('settings.prompts.manage'),
icon: <Pencil />,
action: () => setIsManageModalOpen(true)
action: openManageModal
})

newList.push({
label: t('settings.prompts.add') + '...',
icon: <Plus />,
action: () => setIsAddModalOpen(true)
action: openAddModal
})

return newList
}, [handleItemSelect, isPromptsLoading, promptItems, promptsError, t])
}, [handleItemSelect, isPromptsLoading, openAddModal, openManageModal, promptItems, promptsError, t])

const quickPanelOpenOptions = useMemo<QuickPanelOpenOptions>(
() => ({
Expand Down Expand Up @@ -193,8 +227,8 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
isAddModalOpen,
isCreatingPrompt,
isManageModalOpen,
setIsManageModalOpen,
setIsAddModalOpen
closeAddModal,
handleManageModalOpenChange
}
}

Expand All @@ -203,25 +237,25 @@ const QuickPhrasesModal = ({
isAddModalOpen,
isCreatingPrompt,
isManageModalOpen,
setIsManageModalOpen,
setIsAddModalOpen
closeAddModal,
handleManageModalOpenChange
}: Pick<
ReturnType<typeof useQuickPhrasesToolController>,
| 'handleAddModalSave'
| 'isAddModalOpen'
| 'isCreatingPrompt'
| 'isManageModalOpen'
| 'setIsAddModalOpen'
| 'setIsManageModalOpen'
| 'closeAddModal'
| 'handleManageModalOpenChange'
>) => (
<>
<PromptEditDialog
open={isAddModalOpen}
saving={isCreatingPrompt}
onSave={handleAddModalSave}
onCancel={() => setIsAddModalOpen(false)}
onCancel={closeAddModal}
/>
<PromptManagementDialog open={isManageModalOpen} onOpenChange={setIsManageModalOpen} />
<PromptManagementDialog open={isManageModalOpen} onOpenChange={handleManageModalOpenChange} />
</>
)

Expand Down
104 changes: 63 additions & 41 deletions src/renderer/components/composer/tools/components/WebSearchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { isGemini3Model, isGeminiModel, isGPT5SeriesReasoningModel, isOpenAIWebS
import { isGeminiWebSearchProvider } from '@shared/utils/provider'
import { useNavigate } from '@tanstack/react-router'
import { Globe } from 'lucide-react'
import type { FC } from 'react'
import type { FC, MouseEventHandler } from 'react'
import { memo, useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'

Expand Down Expand Up @@ -76,44 +76,59 @@ const useWebSearchToolController = ({ assistantId, launcher }: Props) => {
: undefined
const isDisabled = Boolean(disabledReason)

const onClick = useCallback(() => {
if (!assistant || !model) {
window.toast.error(t('error.model.not_exists'))
return
}
if (enableWebSearch) {
void updateAssistant({ settings: { enableWebSearch: false } })
return
}

// Built-in web search bypasses the external-provider requirement; the
// toggle simply flips the assistant flag and the model handles search.
if (!hasBuiltinWebSearch && !activeProviderId) {
window.modal.confirm({
centered: true,
title: t('settings.tool.websearch.search_provider'),
content: t('settings.tool.websearch.search_provider_placeholder'),
onOk: () => navigate({ to: '/settings/websearch' })
})
return
}

if (disabledReason) {
return
}

void updateAssistant({ settings: { enableWebSearch: true } })
}, [
activeProviderId,
assistant,
disabledReason,
enableWebSearch,
hasBuiltinWebSearch,
navigate,
t,
updateAssistant,
model
])
const onClick = useCallback(
(restoreFocus?: () => void) => {
if (!assistant || !model) {
window.toast.error(t('error.model.not_exists'))
return
}
if (enableWebSearch) {
void updateAssistant({ settings: { enableWebSearch: false } })
return
}

// Built-in web search bypasses the external-provider requirement; the
// toggle simply flips the assistant flag and the model handles search.
if (!hasBuiltinWebSearch && !activeProviderId) {
let skipFocusRestore = false

window.modal.confirm({
centered: true,
title: t('settings.tool.websearch.search_provider'),
content: t('settings.tool.websearch.search_provider_placeholder'),
afterClose: restoreFocus
? () => {
if (!skipFocusRestore) {
window.requestAnimationFrame(restoreFocus)
}
}
: undefined,
onOk: () => {
skipFocusRestore = true
return navigate({ to: '/settings/websearch' })
}
})
return
}

if (disabledReason) {
return
}

void updateAssistant({ settings: { enableWebSearch: true } })
},
[
activeProviderId,
assistant,
disabledReason,
enableWebSearch,
hasBuiltinWebSearch,
navigate,
t,
updateAssistant,
model
]
)

const ariaLabel = enableWebSearch ? t('common.close') : t('chat.input.web_search.label')
const tooltipTitle = disabledReason ?? ariaLabel
Expand All @@ -134,7 +149,7 @@ const useWebSearchToolController = ({ assistantId, launcher }: Props) => {
active: enableWebSearch,
disabled: isDisabled,
disabledReason,
action: onClick
action: ({ inputAdapter }) => onClick(inputAdapter?.focus)
}
])
}, [disabledReason, enableWebSearch, icon, isDisabled, launcher, onClick, t])
Expand All @@ -149,11 +164,18 @@ export const WebSearchToolRuntime: FC<Props> = (props) => {

const WebSearchButton: FC<Props> = (props) => {
const { ariaLabel, enableWebSearch, icon, isDisabled, onClick, tooltipTitle } = useWebSearchToolController(props)
const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>(
(event) => {
const trigger = event.currentTarget
onClick(() => trigger.focus())
},
[onClick]
)

return (
<Tooltip placement="top" content={tooltipTitle}>
<ActionIconButton
onClick={onClick}
onClick={handleClick}
active={enableWebSearch}
aria-label={ariaLabel}
aria-pressed={enableWebSearch}
Expand Down
Loading