diff --git a/electron/main/filesystem/filesystem.ts b/electron/main/filesystem/filesystem.ts index 4070c827..199a0448 100644 --- a/electron/main/filesystem/filesystem.ts +++ b/electron/main/filesystem/filesystem.ts @@ -4,9 +4,11 @@ import * as path from 'path' import chokidar from 'chokidar' import { BrowserWindow } from 'electron' +import Store from 'electron-store' import { FileInfo, FileInfoTree } from './types' import addExtensionToFilenameIfNoExtensionPresent from '../path/path' import { isFileNodeDirectory } from '../../../shared/utils' +import { StoreSchema, StoreKeys } from '../electron-store/storeConfig' export const markdownExtensions = ['.md', '.markdown', '.mdown', '.mkdn', '.mkd'] @@ -194,3 +196,18 @@ export function splitDirectoryPathIntoBaseAndRepo(fullPath: string) { return { localModelPath, repoName } } + +export const searchFiles = async (store: Store, searchTerm: string): Promise => { + const vaultDirectory = store.get(StoreKeys.DirectoryFromPreviousSession) + if (!vaultDirectory || typeof vaultDirectory !== 'string') { + throw new Error('No valid vault directory found') + } + + const allFiles = GetFilesInfoList(vaultDirectory) + + const searchTermLower = searchTerm.toLowerCase() + + const matchingFiles = allFiles.filter((file) => file.name.toLowerCase().startsWith(searchTermLower)) + + return matchingFiles.map((file) => file.path) +} diff --git a/electron/main/filesystem/ipcHandlers.ts b/electron/main/filesystem/ipcHandlers.ts index 4b7860fd..4c6f7d89 100644 --- a/electron/main/filesystem/ipcHandlers.ts +++ b/electron/main/filesystem/ipcHandlers.ts @@ -8,7 +8,13 @@ import WindowsManager from '../common/windowManager' import { StoreSchema } from '../electron-store/storeConfig' import { handleFileRename, updateFileInTable } from '../vector-database/tableHelperFunctions' -import { GetFilesInfoTree, createFileRecursive, isHidden, GetFilesInfoListForListOfPaths } from './filesystem' +import { + GetFilesInfoTree, + createFileRecursive, + isHidden, + GetFilesInfoListForListOfPaths, + searchFiles, +} from './filesystem' import { FileInfoTree, WriteFileProps, RenameFileProps, FileInfoWithContent } from './types' const registerFileHandlers = (store: Store, _windowsManager: WindowsManager) => { @@ -149,6 +155,10 @@ const registerFileHandlers = (store: Store, _windowsManager: Window } return [] }) + + ipcMain.handle('search-files', async (_event, searchTerm: string) => { + return searchFiles(store, searchTerm) + }) } export default registerFileHandlers diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 88da1001..6507af80 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -92,6 +92,7 @@ const fileSystem = { deleteFile: createIPCHandler<(filePath: string) => Promise>('delete-file'), getAllFilenamesInDirectory: createIPCHandler<(dirName: string) => Promise>('get-files-in-directory'), getFiles: createIPCHandler<(filePaths: string[]) => Promise>('get-files'), + searchFiles: createIPCHandler<(searchTerm: string) => Promise>('search-files'), } const path = { diff --git a/src/components/Chat/ChatInput.tsx b/src/components/Chat/ChatInput.tsx index 00808157..3d22bc00 100644 --- a/src/components/Chat/ChatInput.tsx +++ b/src/components/Chat/ChatInput.tsx @@ -1,6 +1,6 @@ -import React from 'react' - +import React, { useRef, useState } from 'react' import { PiPaperPlaneRight } from 'react-icons/pi' +import FileAutocomplete from './FileAutocomplete' import { AgentConfig, LoadingState } from '../../lib/llm/types' import { Button } from '../ui/button' import LLMSelectOrButton from '../Settings/LLMSettings/LLMSelectOrButton' @@ -28,6 +28,88 @@ const ChatInput: React.FC = ({ agentConfig, setAgentConfig, }) => { + const [showFileAutocomplete, setShowFileAutocomplete] = useState(false) + const [autocompletePosition, setAutocompletePosition] = useState({ top: 0, left: 0 }) + const [searchTerm, setSearchTerm] = useState('') + const textareaRef = useRef(null) + + const getCaretCoordinates = (element: HTMLTextAreaElement) => { + const { selectionStart, value } = element + const textBeforeCaret = value.substring(0, selectionStart) + + // Create a hidden div with the same styling as textarea + const mirror = document.createElement('div') + mirror.style.cssText = window.getComputedStyle(element).cssText + mirror.style.height = 'auto' + mirror.style.position = 'absolute' + mirror.style.visibility = 'hidden' + mirror.style.whiteSpace = 'pre-wrap' + document.body.appendChild(mirror) + + // Create a span for the text before caret + const textNode = document.createTextNode(textBeforeCaret) + const span = document.createElement('span') + span.appendChild(textNode) + mirror.appendChild(span) + + // Get coordinates + const coordinates = { + top: span.offsetTop + parseInt(window.getComputedStyle(element).lineHeight, 10) / 2, + left: span.offsetLeft, + } + + // Clean up + document.body.removeChild(mirror) + + return coordinates + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (userTextFieldInput && !e.shiftKey && e.key === 'Enter') { + e.preventDefault() + handleSubmitNewMessage() + } else if (e.key === '@') { + const rect = e.currentTarget.getBoundingClientRect() + const position = getCaretCoordinates(e.currentTarget) + setAutocompletePosition({ + top: rect.top + position.top, + left: rect.left + position.left, + }) + setShowFileAutocomplete(true) + // console.log('showFileAutocomplete', showFileAutocomplete) + } else if (showFileAutocomplete && e.key === 'Escape') { + setShowFileAutocomplete(false) + } + } + + const handleInput = (e: React.ChangeEvent) => { + const { value } = e.target + setUserTextFieldInput(value) + + // Handle @ mentions + const lastAtIndex = value.lastIndexOf('@') + if (lastAtIndex !== -1 && lastAtIndex < value.length) { + const searchText = value.slice(lastAtIndex + 1).split(/\s/)[0] + setSearchTerm(searchText) + } else { + setShowFileAutocomplete(false) + } + + // Adjust textarea height + e.target.style.height = 'auto' + e.target.style.height = `${Math.min(e.target.scrollHeight, 160)}px` + } + + const handleFileSelect = (filePath: string) => { + const lastAtIndex = userTextFieldInput.lastIndexOf('@') + const newValue = `${userTextFieldInput.slice(0, lastAtIndex)}@${filePath} ${userTextFieldInput.slice( + lastAtIndex + searchTerm.length + 1, + )}` + + setUserTextFieldInput(newValue) + setShowFileAutocomplete(false) + } + // const [useStream, setUseStream] = React.useState(true) const handleDbSearchToggle = (checked: boolean) => { @@ -49,7 +131,7 @@ const ChatInput: React.FC = ({ return (
-
+
{/* */}