chore: conditianal attachment and drag file to chat input

This commit is contained in:
Faisal Amir 2025-08-19 22:36:20 +07:00
parent e3eb8e909b
commit 80dc491f9d
3 changed files with 72 additions and 14 deletions

View File

@ -34,6 +34,7 @@ import DropdownModelProvider from '@/containers/DropdownModelProvider'
import { ModelLoader } from '@/containers/loaders/ModelLoader' import { ModelLoader } from '@/containers/loaders/ModelLoader'
import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable' import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable'
import { getConnectedServers } from '@/services/mcp' import { getConnectedServers } from '@/services/mcp'
import { checkMmprojExists } from '@/services/models'
type ChatInputProps = { type ChatInputProps = {
className?: string className?: string
@ -71,6 +72,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
>([]) >([])
const [connectedServers, setConnectedServers] = useState<string[]>([]) const [connectedServers, setConnectedServers] = useState<string[]>([])
const [isDragOver, setIsDragOver] = useState(false) const [isDragOver, setIsDragOver] = useState(false)
const [hasMmproj, setHasMmproj] = useState(false)
// Check for connected MCP servers // Check for connected MCP servers
useEffect(() => { useEffect(() => {
@ -92,6 +94,25 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
return () => clearInterval(intervalId) return () => clearInterval(intervalId)
}, []) }, [])
// Check for mmproj existence when model changes
useEffect(() => {
const checkMmprojSupport = async () => {
if (selectedModel?.id) {
try {
const exists = await checkMmprojExists(selectedModel.id)
setHasMmproj(exists)
} catch (error) {
console.error('Error checking mmproj:', error)
setHasMmproj(false)
}
} else {
setHasMmproj(false)
}
}
checkMmprojSupport()
}, [selectedModel?.id])
// Check if there are active MCP servers // Check if there are active MCP servers
const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0 const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0
@ -283,7 +304,10 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
const handleDragEnter = (e: React.DragEvent) => { const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setIsDragOver(true) // Only allow drag if model supports mmproj
if (hasMmproj) {
setIsDragOver(true)
}
} }
const handleDragLeave = (e: React.DragEvent) => { const handleDragLeave = (e: React.DragEvent) => {
@ -301,7 +325,9 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
// Ensure drag state is maintained during drag over // Ensure drag state is maintained during drag over
setIsDragOver(true) if (hasMmproj) {
setIsDragOver(true)
}
} }
const handleDrop = (e: React.DragEvent) => { const handleDrop = (e: React.DragEvent) => {
@ -309,6 +335,11 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
e.stopPropagation() e.stopPropagation()
setIsDragOver(false) setIsDragOver(false)
// Only allow drop if model supports mmproj
if (!hasMmproj) {
return
}
// Check if dataTransfer exists (it might not in some Tauri scenarios) // Check if dataTransfer exists (it might not in some Tauri scenarios)
if (!e.dataTransfer) { if (!e.dataTransfer) {
console.warn('No dataTransfer available in drop event') console.warn('No dataTransfer available in drop event')
@ -332,6 +363,11 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
const clipboardItems = e.clipboardData?.items const clipboardItems = e.clipboardData?.items
if (!clipboardItems) return if (!clipboardItems) return
// Only allow paste if model supports mmproj
if (!hasMmproj) {
return
}
const imageItems = Array.from(clipboardItems).filter((item) => const imageItems = Array.from(clipboardItems).filter((item) =>
item.type.startsWith('image/') item.type.startsWith('image/')
) )
@ -390,11 +426,11 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
isFocused && 'ring-1 ring-main-view-fg/10', isFocused && 'ring-1 ring-main-view-fg/10',
isDragOver && 'ring-2 ring-accent border-accent' isDragOver && 'ring-2 ring-accent border-accent'
)} )}
data-drop-zone="true" data-drop-zone={hasMmproj ? "true" : undefined}
onDragEnter={handleDragEnter} onDragEnter={hasMmproj ? handleDragEnter : undefined}
onDragLeave={handleDragLeave} onDragLeave={hasMmproj ? handleDragLeave : undefined}
onDragOver={handleDragOver} onDragOver={hasMmproj ? handleDragOver : undefined}
onDrop={handleDrop} onDrop={hasMmproj ? handleDrop : undefined}
> >
{uploadedFiles.length > 0 && ( {uploadedFiles.length > 0 && (
<div className="flex gap-3 items-center p-2 pb-0"> <div className="flex gap-3 items-center p-2 pb-0">
@ -455,7 +491,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
// When Shift+Enter is pressed, a new line is added (default behavior) // When Shift+Enter is pressed, a new line is added (default behavior)
} }
}} }}
onPaste={handlePaste} onPaste={hasMmproj ? handlePaste : undefined}
placeholder={t('common:placeholder.chatInput')} placeholder={t('common:placeholder.chatInput')}
autoFocus autoFocus
spellCheck={spellCheckChatInput} spellCheck={spellCheckChatInput}
@ -489,7 +525,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
/> />
)} )}
{/* File attachment - show only for models with mmproj */} {/* File attachment - show only for models with mmproj */}
{selectedModel?.settings?.offload_mmproj && ( {hasMmproj && (
<div <div
className="h-6 p-1 ml-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1" className="h-6 p-1 ml-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1"
onClick={handleAttachmentClick} onClick={handleAttachmentClick}

View File

@ -19,7 +19,7 @@ import { localStorageKey } from '@/constants/localStorage'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import { useFavoriteModel } from '@/hooks/useFavoriteModel' import { useFavoriteModel } from '@/hooks/useFavoriteModel'
import { predefinedProviders } from '@/consts/providers' import { predefinedProviders } from '@/consts/providers'
import { checkMmprojExists } from '@/services/models' import { checkMmprojExistsAndUpdateOffloadMMprojSetting } from '@/services/models'
type DropdownModelProviderProps = { type DropdownModelProviderProps = {
model?: ThreadModel model?: ThreadModel
@ -102,7 +102,7 @@ const DropdownModelProvider = ({
} }
// Check mmproj existence for llamacpp models // Check mmproj existence for llamacpp models
if (model?.provider === 'llamacpp') { if (model?.provider === 'llamacpp') {
await checkMmprojExists( await checkMmprojExistsAndUpdateOffloadMMprojSetting(
model.id as string, model.id as string,
updateProvider, updateProvider,
getProviderByName getProviderByName
@ -114,7 +114,7 @@ const DropdownModelProvider = ({
if (lastUsed && checkModelExists(lastUsed.provider, lastUsed.model)) { if (lastUsed && checkModelExists(lastUsed.provider, lastUsed.model)) {
selectModelProvider(lastUsed.provider, lastUsed.model) selectModelProvider(lastUsed.provider, lastUsed.model)
if (lastUsed.provider === 'llamacpp') { if (lastUsed.provider === 'llamacpp') {
await checkMmprojExists( await checkMmprojExistsAndUpdateOffloadMMprojSetting(
lastUsed.model, lastUsed.model,
updateProvider, updateProvider,
getProviderByName getProviderByName
@ -282,7 +282,7 @@ const DropdownModelProvider = ({
// Check mmproj existence for llamacpp models // Check mmproj existence for llamacpp models
if (searchableModel.provider.provider === 'llamacpp') { if (searchableModel.provider.provider === 'llamacpp') {
await checkMmprojExists( await checkMmprojExistsAndUpdateOffloadMMprojSetting(
searchableModel.model.id, searchableModel.model.id,
updateProvider, updateProvider,
getProviderByName getProviderByName

View File

@ -361,7 +361,7 @@ export const isToolSupported = async (modelId: string): Promise<boolean> => {
* @param getProviderByName - Function to get provider by name * @param getProviderByName - Function to get provider by name
* @returns Promise<{exists: boolean, settingsUpdated: boolean}> - exists: true if mmproj.gguf exists, settingsUpdated: true if settings were modified * @returns Promise<{exists: boolean, settingsUpdated: boolean}> - exists: true if mmproj.gguf exists, settingsUpdated: true if settings were modified
*/ */
export const checkMmprojExists = async ( export const checkMmprojExistsAndUpdateOffloadMMprojSetting = async (
modelId: string, modelId: string,
updateProvider?: (providerName: string, data: Partial<ModelProvider>) => void, updateProvider?: (providerName: string, data: Partial<ModelProvider>) => void,
getProviderByName?: (providerName: string) => ModelProvider | undefined getProviderByName?: (providerName: string) => ModelProvider | undefined
@ -465,3 +465,25 @@ export const checkMmprojExists = async (
} }
return { exists: false, settingsUpdated } return { exists: false, settingsUpdated }
} }
/**
* Checks if mmproj.gguf file exists for a given model ID in the llamacpp provider.
* If mmproj.gguf exists, adds offload_mmproj setting with value true.
* @param modelId - The model ID to check for mmproj.gguf
* @param updateProvider - Function to update the provider state
* @param getProviderByName - Function to get provider by name
* @returns Promise<{exists: boolean, settingsUpdated: boolean}> - exists: true if mmproj.gguf exists, settingsUpdated: true if settings were modified
*/
export const checkMmprojExists = async (modelId: string): Promise<boolean> => {
try {
const engine = getEngine('llamacpp') as AIEngine & {
checkMmprojExists?: (id: string) => Promise<boolean>
}
if (engine && typeof engine.checkMmprojExists === 'function') {
return await engine.checkMmprojExists(modelId)
}
} catch (error) {
console.error(`Error checking mmproj for model ${modelId}:`, error)
}
return false
}