Merge pull request #2234 from danielaskdd/fix-webui

Refact: Frontend UI Fixes and Performance Improvements
This commit is contained in:
Daniel.y 2025-10-17 21:27:45 +08:00 committed by GitHub
commit 8070d0cf32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 84 additions and 50 deletions

5
.gitignore vendored
View file

@ -9,9 +9,10 @@ __pycache__/
# Virtual Environment
.venv/
env/
venv/
*.env*
# Enviroment Variable Files
.env
# Build / Distribution
dist/

View file

@ -1 +1 @@
__api_version__ = "0240"
__api_version__ = "0241"

View file

@ -0,0 +1,4 @@
# Development environment configuration
VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status

View file

@ -1,2 +1,4 @@
# Development environment configuration
VITE_BACKEND_URL=/api
VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status

View file

@ -1,3 +1,3 @@
VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/,/api,/documents,/graphs,/graph,/health,/query,/docs,/openapi.json,/login,/auth-status
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status

View file

@ -175,49 +175,59 @@ const GraphLabels = () => {
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</Button>
<AsyncSelect<string>
key={selectKey} // Force re-render when data changes
className="min-w-[300px]"
triggerClassName="max-h-8"
searchInputClassName="max-h-8"
triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
fetcher={fetchData}
renderOption={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>}
getOptionValue={(item) => item}
getDisplayValue={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>}
notFound={<div className="py-6 text-center text-sm">{t('graphPanel.graphLabels.noLabels')}</div>}
ariaLabel={t('graphPanel.graphLabels.label')}
placeholder={t('graphPanel.graphLabels.placeholder')}
searchPlaceholder={t('graphPanel.graphLabels.placeholder')}
noResultsMessage={t('graphPanel.graphLabels.noLabels')}
value={label !== null ? label : '*'}
onChange={(newLabel) => {
const currentLabel = useSettingsStore.getState().queryLabel;
<div className="w-full min-w-[280px] max-w-[500px]">
<AsyncSelect<string>
key={selectKey} // Force re-render when data changes
className="min-w-[300px]"
triggerClassName="max-h-8 w-full overflow-hidden"
searchInputClassName="max-h-8"
triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
fetcher={fetchData}
renderOption={(item) => (
<div className="truncate" title={item}>
{item}
</div>
)}
getOptionValue={(item) => item}
getDisplayValue={(item) => (
<div className="min-w-0 flex-1 truncate text-left" title={item}>
{item}
</div>
)}
notFound={<div className="py-6 text-center text-sm">{t('graphPanel.graphLabels.noLabels')}</div>}
ariaLabel={t('graphPanel.graphLabels.label')}
placeholder={t('graphPanel.graphLabels.placeholder')}
searchPlaceholder={t('graphPanel.graphLabels.placeholder')}
noResultsMessage={t('graphPanel.graphLabels.noLabels')}
value={label !== null ? label : '*'}
onChange={(newLabel) => {
const currentLabel = useSettingsStore.getState().queryLabel;
// select the last item means query all
if (newLabel === '...') {
newLabel = '*';
}
// select the last item means query all
if (newLabel === '...') {
newLabel = '*';
}
// Handle reselecting the same label
if (newLabel === currentLabel && newLabel !== '*') {
newLabel = '*';
}
// Handle reselecting the same label
if (newLabel === currentLabel && newLabel !== '*') {
newLabel = '*';
}
// Add selected label to search history (except for special cases)
if (newLabel && newLabel !== '*' && newLabel !== '...' && newLabel.trim() !== '') {
SearchHistoryManager.addToHistory(newLabel);
}
// Add selected label to search history (except for special cases)
if (newLabel && newLabel !== '*' && newLabel !== '...' && newLabel.trim() !== '') {
SearchHistoryManager.addToHistory(newLabel);
}
// Reset graphDataFetchAttempted flag to ensure data fetch is triggered
useGraphStore.getState().setGraphDataFetchAttempted(false);
// Reset graphDataFetchAttempted flag to ensure data fetch is triggered
useGraphStore.getState().setGraphDataFetchAttempted(false);
// Update the label to trigger data loading
useSettingsStore.getState().setQueryLabel(newLabel);
}}
clearable={false} // Prevent clearing value on reselect
debounceTime={500}
/>
// Update the label to trigger data loading
useSettingsStore.getState().setQueryLabel(newLabel);
}}
clearable={false} // Prevent clearing value on reselect
debounceTime={500}
/>
</div>
</div>
)
}

View file

@ -45,7 +45,13 @@ export type MessageWithError = Message & {
}
// Restore original component definition and export
export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop
export const ChatMessage = ({
message,
isTabActive = true
}: {
message: MessageWithError
isTabActive?: boolean
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
const [katexPlugin, setKatexPlugin] = useState<((options?: KaTeXOptions) => any) | null>(null)
@ -148,8 +154,13 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
} rounded-lg px-4 py-2`}
>
{/* Thinking process display - only for assistant messages */}
{/* Always render to prevent layout shift when switching tabs */}
{message.role === 'assistant' && (isThinking || thinkingTime !== null) && (
<div className="mb-2">
<div className={cn(
'mb-2',
// Reduce visual priority in inactive tabs while maintaining layout
!isTabActive && 'opacity-50'
)}>
<div
className="flex items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200 text-sm cursor-pointer select-none"
onClick={() => {
@ -161,7 +172,8 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
>
{isThinking ? (
<>
<LoaderIcon className="mr-2 size-4 animate-spin" />
{/* Only show spinner animation in active tab to save resources */}
{isTabActive && <LoaderIcon className="mr-2 size-4 animate-spin" />}
<span>{t('retrievePanel.chatMessage.thinking')}</span>
</>
) : (
@ -247,11 +259,12 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
</ReactMarkdown>
</div>
)}
{(() => {
{/* Loading indicator - only show in active tab */}
{isTabActive && (() => {
// More comprehensive loading state check
const hasVisibleContent = finalDisplayContent && finalDisplayContent.trim() !== '';
const isLoadingState = !hasVisibleContent && !isThinking && !thinkingTime;
return isLoadingState && <LoaderIcon className="animate-spin duration-2000" />;
return isLoadingState && <LoaderIcon className="animate-spin duration-2000" />
})()}
</div>
)

View file

@ -103,6 +103,10 @@ const parseCOTContent = (content: string) => {
export default function RetrievalTesting() {
const { t } = useTranslation()
// Get current tab to determine if this tab is active (for performance optimization)
const currentTab = useSettingsStore.use.currentTab()
const isRetrievalTabActive = currentTab === 'retrieval'
const [messages, setMessages] = useState<MessageWithError[]>(() => {
try {
const history = useSettingsStore.getState().retrievalHistory || []
@ -716,7 +720,7 @@ export default function RetrievalTesting() {
<CopyIcon className="size-4" />
</Button>
)}
<ChatMessage message={message} />
<ChatMessage message={message} isTabActive={isRetrievalTabActive} />
{message.role === 'assistant' && (
<Button
onClick={() => handleCopyMessage(message)}

View file

@ -40,7 +40,7 @@ export default defineConfig({
changeOrigin: true,
rewrite: endpoint === '/api' ?
(path) => path.replace(/^\/api/, '') :
endpoint === '/docs' || endpoint === '/openapi.json' ?
endpoint === '/docs' || endpoint === '/redoc' || endpoint === '/openapi.json' ?
(path) => path : undefined
}
])