diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index 1b4dc4800..745fb3d13 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -95,6 +95,10 @@ export class KhojChatView extends KhojPaneView { private modeDropdown: HTMLElement | null = null; private selectedOptionIndex: number = -1; private isStreaming: boolean = false; // Flag to track streaming state + // File filter autocomplete properties + private fileFilterDropdown: HTMLElement | null = null; + private fileFilterSuggestions: string[] = []; + private selectedFileFilterIndex: number = -1; // Disabled retry logic for now. Can re-enable once: // 1. Handle chat history clutter @@ -1850,6 +1854,15 @@ export class KhojChatView extends KhojPaneView { this.hideModeDropdown(); } + // Check for file: filter pattern and show file filter dropdown + const fileFilterMatch = chatInput.value.match(/file:(\S*)$/); + if (fileFilterMatch) { + const filterQuery = fileFilterMatch[1] || ''; + this.showFileFilterDropdown(chatInput, filterQuery); + } else if (this.fileFilterDropdown) { + this.hideFileFilterDropdown(); + } + this.autoResize(); } @@ -2568,5 +2581,76 @@ export class KhojChatView extends KhojPaneView { this.modeDropdown.style.display = "none"; this.selectedOptionIndex = -1; } + + // File filter autocomplete methods + private async fetchUserFiles(): Promise { + try { + const response = await fetch(`${this.setting.khojUrl}/api/content/files`, { + headers: { 'Authorization': `Bearer ${this.setting.khojApiKey}` } + }); + if (response.ok) { + const data = await response.json(); + return data.files?.map((f: any) => f.file_name) || []; + } + } catch (error) { + console.error('Error fetching files:', error); + } + return []; + } + + private onChatInput() +onChatInput() { + if (this.fileFilterDropdown) { + this.fileFilterDropdown.style.display = "none"; + this.selectedFileFilterIndex = -1; + } + + private async showFileFilterDropdown(inputEl: HTMLTextAreaElement, filterQuery: string) { + // Fetch files if not already loaded + if (this.fileFilterSuggestions.length === 0) { + this.fileFilterSuggestions = await this.fetchUserFiles(); + } + + // Filter files based on query + const filteredFiles = filterQuery + ? this.fileFilterSuggestions.filter(f => f.toLowerCase().includes(filterQuery.toLowerCase())) + : this.fileFilterSuggestions; + + if (filteredFiles.length === 0) { + this.hideFileFilterDropdown(); + return; + } + + // Create dropdown if it doesn't exist + if (!this.fileFilterDropdown) { + this.fileFilterDropdown = this.contentEl.createDiv({ cls: "khoj-file-filter-dropdown" }); + this.fileFilterDropdown.style.position = "absolute"; + this.fileFilterDropdown.style.zIndex = "1000"; + } + + // Position dropdown above input + const inputRect = inputEl.getBoundingClientRect(); + const containerRect = this.contentEl.getBoundingClientRect(); + this.fileFilterDropdown.style.left = `${inputRect.left - containerRect.left}px`; + this.fileFilterDropdown.style.bottom = `${containerRect.bottom - inputRect.top + 4}px`; + this.fileFilterDropdown.style.width = `${inputRect.width}px`; + + // Clear and populate dropdown + this.fileFilterDropdown.empty(); + filteredFiles.slice(0, 10).forEach((file, index) => { + const option = this.fileFilterDropdown!.createDiv({ cls: "khoj-file-filter-option" }); + option.textContent = file.split('/').pop() || file; + option.title = file; + option.addEventListener('click', () => { + // Replace file: pattern with selected file + inputEl.value = inputEl.value.replace(/file:\S*$/, `file:"${file}" `); + this.hideFileFilterDropdown(); + inputEl.focus(); + }); + }); + + this.fileFilterDropdown.style.display = "block"; + } + } } } diff --git a/src/interface/web/app/components/chatInputArea/chatInputArea.tsx b/src/interface/web/app/components/chatInputArea/chatInputArea.tsx index 408eea1a4..a6be35a0f 100644 --- a/src/interface/web/app/components/chatInputArea/chatInputArea.tsx +++ b/src/interface/web/app/components/chatInputArea/chatInputArea.tsx @@ -113,6 +113,9 @@ export const ChatInputArea = forwardRef((pr const [isDragAndDropping, setIsDragAndDropping] = useState(false); const [showCommandList, setShowCommandList] = useState(false); + const [showFileFilter, setShowFileFilter] = useState(false); + const [fileFilterSuggestions, setFileFilterSuggestions] = useState([]); + const [fileFilterQuery, setFileFilterQuery] = useState(""); const [useResearchMode, setUseResearchMode] = useState( props.isResearchModeEnabled || false, ); @@ -242,6 +245,18 @@ export const ChatInputArea = forwardRef((pr uploadFiles(event.dataTransfer.files); } + async function fetchUserFiles() { + try { + const response = await fetch("/api/content/files"); + if (!response.ok) throw new Error("Failed to fetch files"); + const data = await response.json(); + return data.files.map((file: any) => file.file_name); + } catch (error) { + console.error("Error fetching files:", error); + return []; + } + } + function uploadFiles(files: FileList) { if (!props.isLoggedIn) { setLoginRedirectMessage("Please login to chat with your files"); @@ -415,6 +430,27 @@ export const ChatInputArea = forwardRef((pr setShowCommandList(true); } else { setShowCommandList(false); + + // Detect file: filter pattern + const fileFilterMatch = message.match(/file:(\S*)$/); + if (fileFilterMatch) { + const query = fileFilterMatch[1]; + setFileFilterQuery(query); + if (!showFileFilter) { + fetchUserFiles().then(files => { + setFileFilterSuggestions(files); + setShowFileFilter(true); + }); + } else { + // Filter existing suggestions + fetchUserFiles().then(allFiles => { + const filtered = allFiles.filter(f => f.toLowerCase().includes(query.toLowerCase())); + setFileFilterSuggestions(filtered); + }); + } + } else { + setShowFileFilter(false); + } } }, [message]); @@ -554,6 +590,39 @@ export const ChatInputArea = forwardRef((pr )} + {showFileFilter && ( + + + e.preventDefault()} + className={`${props.isMobileWidth ? "w-[100vw]" : "w-full"} rounded-md`} + side="bottom" + align="center" + sideOffset={props.conversationId ? 0 : 80} + > + + + + No files found. + + {fileFilterSuggestions.map((filename) => ( + { + const newMessage = message.replace(/file:\S*$/, `file:${filename} `); + setMessage(newMessage); + setShowFileFilter(false); + }} + > + {filename} + + ))} + + + + + + )}
{imageUploaded &&