Merge pull request #2131 from danielaskdd/fix-autocomplete
Fix: Restore browser autocomplete functionality in message input box
This commit is contained in:
commit
7d785cde7b
6 changed files with 195 additions and 71 deletions
|
|
@ -1 +1 @@
|
||||||
__api_version__ = "0227"
|
__api_version__ = "0228"
|
||||||
|
|
|
||||||
12
lightrag/api/webui/assets/feature-retrieval-BJ2Rchnh.js
generated
Normal file
12
lightrag/api/webui/assets/feature-retrieval-BJ2Rchnh.js
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
||||||
import{j as o,Y as ld,O as fg,k as dg,u as ad,Z as mg,c as hg,l as gg,g as pg,S as yg,T as vg,n as bg,m as nd,o as Sg,p as Tg,$ as ud,a0 as id,a1 as cd,a2 as xg}from"./ui-vendor-CeCm8EER.js";import{d as Ag,h as Dg,r as E,u as sd,H as Ng,i as Eg,j as kf}from"./react-vendor-DEwriMA6.js";import{N as we,c as Ve,ae as od,u as qt,M as st,af as rd,ag as fd,I as us,B as Cn,D as Mg,l as zg,m as Cg,n as Og,o as _g,ah as jg,ai as Rg,aj as Ug,ak as Hg,al as Bt,am as dd,an as ss,ao as is,a1 as Lg,a2 as Bg,a3 as qg,a4 as Gg,ap as Yg,aq as Xg,ar as md,as as wg,at as hd,au as Vg,av as gd,d as Qg,R as Kg,V as Zg,g as En,aw as kg,ax as Jg,ay as Fg}from"./feature-graph-BoGscR3f.js";import{S as Jf,a as Ff,b as Pf,c as $f,e as rl,D as Pg}from"./feature-documents-mDEHQl9_.js";import{R as $g}from"./feature-retrieval-C3jYGwxd.js";import{i as cs}from"./utils-vendor-BysuhMZA.js";import"./graph-vendor-B-X5JegA.js";import"./mermaid-vendor-DdWbvlft.js";import"./markdown-vendor-DmIvJdn7.js";(function(){const y=document.createElement("link").relList;if(y&&y.supports&&y.supports("modulepreload"))return;for(const N of document.querySelectorAll('link[rel="modulepreload"]'))d(N);new MutationObserver(N=>{for(const _ of N)if(_.type==="childList")for(const H of _.addedNodes)H.tagName==="LINK"&&H.rel==="modulepreload"&&d(H)}).observe(document,{childList:!0,subtree:!0});function x(N){const _={};return N.integrity&&(_.integrity=N.integrity),N.referrerPolicy&&(_.referrerPolicy=N.referrerPolicy),N.crossOrigin==="use-credentials"?_.credentials="include":N.crossOrigin==="anonymous"?_.credentials="omit":_.credentials="same-origin",_}function d(N){if(N.ep)return;N.ep=!0;const _=x(N);fetch(N.href,_)}})();var ls={exports:{}},Mn={},as={exports:{}},ns={};/**
|
import{j as o,Y as ld,O as fg,k as dg,u as ad,Z as mg,c as hg,l as gg,g as pg,S as yg,T as vg,n as bg,m as nd,o as Sg,p as Tg,$ as ud,a0 as id,a1 as cd,a2 as xg}from"./ui-vendor-CeCm8EER.js";import{d as Ag,h as Dg,r as E,u as sd,H as Ng,i as Eg,j as kf}from"./react-vendor-DEwriMA6.js";import{N as we,c as Ve,ae as od,u as qt,M as st,af as rd,ag as fd,I as us,B as Cn,D as Mg,l as zg,m as Cg,n as Og,o as _g,ah as jg,ai as Rg,aj as Ug,ak as Hg,al as Bt,am as dd,an as ss,ao as is,a1 as Lg,a2 as Bg,a3 as qg,a4 as Gg,ap as Yg,aq as Xg,ar as md,as as wg,at as hd,au as Vg,av as gd,d as Qg,R as Kg,V as Zg,g as En,aw as kg,ax as Jg,ay as Fg}from"./feature-graph-BoGscR3f.js";import{S as Jf,a as Ff,b as Pf,c as $f,e as rl,D as Pg}from"./feature-documents-mDEHQl9_.js";import{R as $g}from"./feature-retrieval-BJ2Rchnh.js";import{i as cs}from"./utils-vendor-BysuhMZA.js";import"./graph-vendor-B-X5JegA.js";import"./mermaid-vendor-DdWbvlft.js";import"./markdown-vendor-DmIvJdn7.js";(function(){const y=document.createElement("link").relList;if(y&&y.supports&&y.supports("modulepreload"))return;for(const N of document.querySelectorAll('link[rel="modulepreload"]'))d(N);new MutationObserver(N=>{for(const _ of N)if(_.type==="childList")for(const H of _.addedNodes)H.tagName==="LINK"&&H.rel==="modulepreload"&&d(H)}).observe(document,{childList:!0,subtree:!0});function x(N){const _={};return N.integrity&&(_.integrity=N.integrity),N.referrerPolicy&&(_.referrerPolicy=N.referrerPolicy),N.crossOrigin==="use-credentials"?_.credentials="include":N.crossOrigin==="anonymous"?_.credentials="omit":_.credentials="same-origin",_}function d(N){if(N.ep)return;N.ep=!0;const _=x(N);fetch(N.href,_)}})();var ls={exports:{}},Mn={},as={exports:{}},ns={};/**
|
||||||
* @license React
|
* @license React
|
||||||
* scheduler.production.js
|
* scheduler.production.js
|
||||||
*
|
*
|
||||||
4
lightrag/api/webui/index.html
generated
4
lightrag/api/webui/index.html
generated
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="/webui/assets/index-C_QOHHST.js"></script>
|
<script type="module" crossorigin src="/webui/assets/index-B1Bq3Rzj.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js">
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js">
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js">
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-mDEHQl9_.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-mDEHQl9_.js">
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-DdWbvlft.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-DdWbvlft.js">
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-DmIvJdn7.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-DmIvJdn7.js">
|
||||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-C3jYGwxd.js">
|
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-BJ2Rchnh.js">
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css">
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-D1a3WRz5.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/index-D1a3WRz5.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import Textarea from '@/components/ui/Textarea'
|
import Textarea from '@/components/ui/Textarea'
|
||||||
|
import Input from '@/components/ui/Input'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { throttle } from '@/lib/utils'
|
import { throttle } from '@/lib/utils'
|
||||||
|
|
@ -116,29 +117,24 @@ export default function RetrievalTesting() {
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [inputError, setInputError] = useState('') // Error message for input
|
const [inputError, setInputError] = useState('') // Error message for input
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
||||||
// Reference to track if we should follow scroll during streaming (using ref for synchronous updates)
|
|
||||||
const shouldFollowScrollRef = useRef(true)
|
|
||||||
const thinkingStartTime = useRef<number | null>(null)
|
|
||||||
const thinkingProcessed = useRef(false)
|
|
||||||
// Reference to track if user interaction is from the form area
|
|
||||||
const isFormInteractionRef = useRef(false)
|
|
||||||
// Reference to track if scroll was triggered programmatically
|
|
||||||
const programmaticScrollRef = useRef(false)
|
|
||||||
// Reference to track if we're currently receiving a streaming response
|
|
||||||
const isReceivingResponseRef = useRef(false)
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// Add cleanup effect for memory leak prevention
|
// Smart switching logic: use Input for single line, Textarea for multi-line
|
||||||
useEffect(() => {
|
const hasMultipleLines = inputValue.includes('\n')
|
||||||
// Component cleanup - reset timer state to prevent memory leaks
|
|
||||||
return () => {
|
// Enhanced event handlers for smart switching
|
||||||
if (thinkingStartTime.current) {
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
thinkingStartTime.current = null;
|
setInputValue(e.target.value)
|
||||||
}
|
if (inputError) setInputError('')
|
||||||
};
|
}, [inputError])
|
||||||
}, []);
|
|
||||||
|
// Unified height adjustment function for textarea
|
||||||
|
const adjustTextareaHeight = useCallback((element: HTMLTextAreaElement) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
element.style.height = 'auto'
|
||||||
|
element.style.height = Math.min(element.scrollHeight, 120) + 'px'
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Scroll to bottom function - restored smooth scrolling with better handling
|
// Scroll to bottom function - restored smooth scrolling with better handling
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
|
|
@ -230,9 +226,11 @@ export default function RetrievalTesting() {
|
||||||
setInputValue('')
|
setInputValue('')
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
// Reset textarea height to minimum after clearing input
|
// Reset input height to minimum after clearing input
|
||||||
if (textareaRef.current) {
|
if (inputRef.current) {
|
||||||
textareaRef.current.style.height = '40px'
|
if ('style' in inputRef.current) {
|
||||||
|
inputRef.current.style.height = '40px'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a function to update the assistant's message
|
// Create a function to update the assistant's message
|
||||||
|
|
@ -390,6 +388,111 @@ export default function RetrievalTesting() {
|
||||||
[inputValue, isLoading, messages, setMessages, t, scrollToBottom]
|
[inputValue, isLoading, messages, setMessages, t, scrollToBottom]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && e.shiftKey) {
|
||||||
|
// Shift+Enter: Insert newline
|
||||||
|
e.preventDefault()
|
||||||
|
const target = e.target as HTMLInputElement | HTMLTextAreaElement
|
||||||
|
const start = target.selectionStart || 0
|
||||||
|
const end = target.selectionEnd || 0
|
||||||
|
const newValue = inputValue.slice(0, start) + '\n' + inputValue.slice(end)
|
||||||
|
setInputValue(newValue)
|
||||||
|
|
||||||
|
// Set cursor position after the newline and adjust height if needed
|
||||||
|
setTimeout(() => {
|
||||||
|
if (target.setSelectionRange) {
|
||||||
|
target.setSelectionRange(start + 1, start + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually trigger height adjustment for textarea after component switch
|
||||||
|
if (inputRef.current && inputRef.current.tagName === 'TEXTAREA') {
|
||||||
|
adjustTextareaHeight(inputRef.current as HTMLTextAreaElement)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
} else if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
// Enter: Submit form
|
||||||
|
e.preventDefault()
|
||||||
|
handleSubmit(e as any)
|
||||||
|
}
|
||||||
|
}, [inputValue, handleSubmit, adjustTextareaHeight])
|
||||||
|
|
||||||
|
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
// Get pasted text content
|
||||||
|
const pastedText = e.clipboardData.getData('text')
|
||||||
|
|
||||||
|
// Check if it contains newlines
|
||||||
|
if (pastedText.includes('\n')) {
|
||||||
|
e.preventDefault() // Prevent default paste behavior
|
||||||
|
|
||||||
|
// Get current cursor position
|
||||||
|
const target = e.target as HTMLInputElement | HTMLTextAreaElement
|
||||||
|
const start = target.selectionStart || 0
|
||||||
|
const end = target.selectionEnd || 0
|
||||||
|
|
||||||
|
// Build new value
|
||||||
|
const newValue = inputValue.slice(0, start) + pastedText + inputValue.slice(end)
|
||||||
|
|
||||||
|
// Update state (this will trigger component switch to Textarea)
|
||||||
|
setInputValue(newValue)
|
||||||
|
|
||||||
|
// Set cursor position to end of pasted content
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current && inputRef.current.setSelectionRange) {
|
||||||
|
const newCursorPosition = start + pastedText.length
|
||||||
|
inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
// If no newlines, let default paste behavior continue
|
||||||
|
}, [inputValue])
|
||||||
|
|
||||||
|
// Effect to handle component switching and maintain focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
// When component type changes, restore focus and cursor position
|
||||||
|
const currentElement = inputRef.current
|
||||||
|
const cursorPosition = currentElement.selectionStart || inputValue.length
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to ensure DOM update is complete
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
currentElement.focus()
|
||||||
|
if (currentElement.setSelectionRange) {
|
||||||
|
currentElement.setSelectionRange(cursorPosition, cursorPosition)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [hasMultipleLines, inputValue.length]) // Include inputValue.length dependency
|
||||||
|
|
||||||
|
// Effect to adjust textarea height when switching to multi-line mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMultipleLines && inputRef.current && inputRef.current.tagName === 'TEXTAREA') {
|
||||||
|
adjustTextareaHeight(inputRef.current as HTMLTextAreaElement)
|
||||||
|
}
|
||||||
|
}, [hasMultipleLines, inputValue, adjustTextareaHeight])
|
||||||
|
|
||||||
|
// Reference to track if we should follow scroll during streaming (using ref for synchronous updates)
|
||||||
|
const shouldFollowScrollRef = useRef(true)
|
||||||
|
const thinkingStartTime = useRef<number | null>(null)
|
||||||
|
const thinkingProcessed = useRef(false)
|
||||||
|
// Reference to track if user interaction is from the form area
|
||||||
|
const isFormInteractionRef = useRef(false)
|
||||||
|
// Reference to track if scroll was triggered programmatically
|
||||||
|
const programmaticScrollRef = useRef(false)
|
||||||
|
// Reference to track if we're currently receiving a streaming response
|
||||||
|
const isReceivingResponseRef = useRef(false)
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Add cleanup effect for memory leak prevention
|
||||||
|
useEffect(() => {
|
||||||
|
// Component cleanup - reset timer state to prevent memory leaks
|
||||||
|
return () => {
|
||||||
|
if (thinkingStartTime.current) {
|
||||||
|
thinkingStartTime.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Add event listeners to detect when user manually interacts with the container
|
// Add event listeners to detect when user manually interacts with the container
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = messagesContainerRef.current;
|
const container = messagesContainerRef.current;
|
||||||
|
|
@ -510,7 +613,16 @@ export default function RetrievalTesting() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2" autoComplete="on">
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex shrink-0 items-center gap-2"
|
||||||
|
autoComplete="on"
|
||||||
|
method="post"
|
||||||
|
action="#"
|
||||||
|
role="search"
|
||||||
|
>
|
||||||
|
{/* Hidden submit button to ensure form meets HTML standards */}
|
||||||
|
<input type="submit" style={{ display: 'none' }} tabIndex={-1} />
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -525,38 +637,47 @@ export default function RetrievalTesting() {
|
||||||
<label htmlFor="query-input" className="sr-only">
|
<label htmlFor="query-input" className="sr-only">
|
||||||
{t('retrievePanel.retrieval.placeholder')}
|
{t('retrievePanel.retrieval.placeholder')}
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
{hasMultipleLines ? (
|
||||||
ref={textareaRef}
|
<Textarea
|
||||||
id="query-input"
|
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
|
||||||
name="query"
|
id="query-input"
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
className="w-full min-h-[40px] max-h-[120px] overflow-y-auto"
|
className="w-full min-h-[40px] max-h-[120px] overflow-y-auto"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
onChange={handleChange}
|
||||||
setInputValue(e.target.value)
|
onKeyDown={handleKeyDown}
|
||||||
if (inputError) setInputError('')
|
onPaste={handlePaste}
|
||||||
}}
|
placeholder={t('retrievePanel.retrieval.placeholder')}
|
||||||
onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
disabled={isLoading}
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
rows={1}
|
||||||
e.preventDefault()
|
style={{
|
||||||
handleSubmit(e as any)
|
resize: 'none',
|
||||||
}
|
height: 'auto',
|
||||||
}}
|
minHeight: '40px',
|
||||||
placeholder={t('retrievePanel.retrieval.placeholder')}
|
maxHeight: '120px'
|
||||||
disabled={isLoading}
|
}}
|
||||||
rows={1}
|
onInput={(e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||||
style={{
|
const target = e.target as HTMLTextAreaElement
|
||||||
resize: 'none',
|
requestAnimationFrame(() => {
|
||||||
height: 'auto',
|
target.style.height = 'auto'
|
||||||
minHeight: '40px',
|
target.style.height = Math.min(target.scrollHeight, 120) + 'px'
|
||||||
maxHeight: '120px'
|
})
|
||||||
}}
|
}}
|
||||||
onInput={(e: React.FormEvent<HTMLTextAreaElement>) => {
|
/>
|
||||||
const target = e.target as HTMLTextAreaElement
|
) : (
|
||||||
target.style.height = 'auto'
|
<Input
|
||||||
target.style.height = Math.min(target.scrollHeight, 120) + 'px'
|
ref={inputRef as React.RefObject<HTMLInputElement>}
|
||||||
}}
|
id="query-input"
|
||||||
/>
|
autoComplete="on"
|
||||||
|
className="w-full"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
placeholder={t('retrievePanel.retrieval.placeholder')}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Error message below input */}
|
{/* Error message below input */}
|
||||||
{inputError && (
|
{inputError && (
|
||||||
<div className="absolute left-0 top-full mt-1 text-xs text-red-500">{inputError}</div>
|
<div className="absolute left-0 top-full mt-1 text-xs text-red-500">{inputError}</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue