auto-git:
[add] src/ChatDatabasePicker.jsx [add] src/useChatLibrarySelection.js [change] src/App.jsx
This commit is contained in:
@@ -3,6 +3,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're
|
|||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import AssistantMessageContent from './AssistantMessageContent'
|
import AssistantMessageContent from './AssistantMessageContent'
|
||||||
|
import ChatDatabasePicker from './ChatDatabasePicker'
|
||||||
import LibraryManager from './LibraryManager'
|
import LibraryManager from './LibraryManager'
|
||||||
import { SettingsPanel, SettingsSidebar } from './SettingsPanels'
|
import { SettingsPanel, SettingsSidebar } from './SettingsPanels'
|
||||||
import { applyColorScheme } from './colorSchemes'
|
import { applyColorScheme } from './colorSchemes'
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
AUDIO_RECORDING_TICK_MS,
|
AUDIO_RECORDING_TICK_MS,
|
||||||
BOTTOM_EPSILON,
|
BOTTOM_EPSILON,
|
||||||
CHAT_LIBRARY_MAP_KEY,
|
|
||||||
DEFAULT_BACKEND_API_URL,
|
DEFAULT_BACKEND_API_URL,
|
||||||
MAX_AUDIO_RECORDING_MS,
|
MAX_AUDIO_RECORDING_MS,
|
||||||
MAX_IMAGE_ATTACHMENT_BYTES,
|
MAX_IMAGE_ATTACHMENT_BYTES,
|
||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
stopMediaStream,
|
stopMediaStream,
|
||||||
supportsAudioInputCapture,
|
supportsAudioInputCapture,
|
||||||
} from './audioInput'
|
} from './audioInput'
|
||||||
|
import { useChatLibrarySelection } from './useChatLibrarySelection'
|
||||||
|
|
||||||
function appendTranscriptToComposer(currentInput, transcript) {
|
function appendTranscriptToComposer(currentInput, transcript) {
|
||||||
const nextTranscript = String(transcript || '').trim()
|
const nextTranscript = String(transcript || '').trim()
|
||||||
|
|||||||
102
src/ChatDatabasePicker.jsx
Normal file
102
src/ChatDatabasePicker.jsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function ChatDatabasePicker({
|
||||||
|
activeSessionId,
|
||||||
|
chatLibrary,
|
||||||
|
chatLibrarySlug,
|
||||||
|
chatLibraryStatusSuffix,
|
||||||
|
isLibrarySyncing,
|
||||||
|
libraries,
|
||||||
|
setChatLibraryForSession,
|
||||||
|
}) {
|
||||||
|
const dbPickerRef = useRef(null)
|
||||||
|
const [isDbPickerOpen, setIsDbPickerOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDbPickerOpen) return
|
||||||
|
|
||||||
|
const onPointerDown = (event) => {
|
||||||
|
if (!dbPickerRef.current?.contains(event.target)) {
|
||||||
|
setIsDbPickerOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', onPointerDown)
|
||||||
|
return () => document.removeEventListener('mousedown', onPointerDown)
|
||||||
|
}, [isDbPickerOpen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDbPickerOpen(false)
|
||||||
|
}, [activeSessionId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="footer-tool-group" ref={dbPickerRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={"db-picker-toggle" + (chatLibrary ? " active" : "")}
|
||||||
|
onClick={() => {
|
||||||
|
if (!activeSessionId) return
|
||||||
|
setIsDbPickerOpen(prev => !prev)
|
||||||
|
}}
|
||||||
|
title={chatLibrary ? `Database: ${chatLibrary.name}${chatLibraryStatusSuffix}` : 'Select database for this chat'}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={isDbPickerOpen}
|
||||||
|
disabled={!activeSessionId}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||||
|
aria-hidden="true">
|
||||||
|
<ellipse cx="12" cy="5" rx="8" ry="3"/>
|
||||||
|
<path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/>
|
||||||
|
<path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{isDbPickerOpen && (
|
||||||
|
<div className="db-picker-menu" role="menu">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={"db-picker-option" + (!chatLibrarySlug ? " selected" : "")}
|
||||||
|
onClick={() => {
|
||||||
|
setChatLibraryForSession(activeSessionId, null)
|
||||||
|
setIsDbPickerOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>No database</span>
|
||||||
|
{!chatLibrarySlug && <span className="db-picker-status">Selected</span>}
|
||||||
|
</button>
|
||||||
|
{libraries.length === 0 ? (
|
||||||
|
<div className="db-picker-empty">No databases yet.</div>
|
||||||
|
) : (
|
||||||
|
libraries.map(library => {
|
||||||
|
const selected = chatLibrarySlug === library.slug
|
||||||
|
const syncing = isLibrarySyncing(library.slug)
|
||||||
|
const status = !library.files?.length
|
||||||
|
? 'Empty'
|
||||||
|
: library.states?.is_indexed
|
||||||
|
? 'Ready'
|
||||||
|
: syncing
|
||||||
|
? 'Syncing'
|
||||||
|
: 'Needs sync'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={library.slug}
|
||||||
|
type="button"
|
||||||
|
className={"db-picker-option" + (selected ? " selected" : "")}
|
||||||
|
disabled={!library.files?.length}
|
||||||
|
onClick={() => {
|
||||||
|
setChatLibraryForSession(activeSessionId, library.slug)
|
||||||
|
setIsDbPickerOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{library.name}</span>
|
||||||
|
<span className="db-picker-status">{selected ? 'Selected' : status}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
124
src/useChatLibrarySelection.js
Normal file
124
src/useChatLibrarySelection.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { CHAT_LIBRARY_MAP_KEY } from './appConfig'
|
||||||
|
|
||||||
|
function libraryJobIsActive(job) {
|
||||||
|
return job?.status === 'queued' || job?.status === 'running'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLibraryStatusSuffix(library, hasActiveJob) {
|
||||||
|
if (!library) return ''
|
||||||
|
if (!library.files?.length) return ' (empty)'
|
||||||
|
if (library.states?.is_indexed) return ''
|
||||||
|
return hasActiveJob ? ' (syncing)' : ' (needs sync)'
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadChatLibraryMap() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CHAT_LIBRARY_MAP_KEY)
|
||||||
|
return raw ? JSON.parse(raw) : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatLibrarySelection({ activeSessionId, libraries, libraryJobs }) {
|
||||||
|
const [chatLibraryBySession, setChatLibraryBySession] = useState(loadChatLibraryMap)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CHAT_LIBRARY_MAP_KEY, JSON.stringify(chatLibraryBySession || {}))
|
||||||
|
} catch {}
|
||||||
|
}, [chatLibraryBySession])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const validSlugs = new Set(libraries.map(library => library.slug))
|
||||||
|
setChatLibraryBySession(prev => {
|
||||||
|
let changed = false
|
||||||
|
const next = {}
|
||||||
|
for (const [sessionId, slug] of Object.entries(prev || {})) {
|
||||||
|
if (validSlugs.has(slug)) {
|
||||||
|
next[sessionId] = slug
|
||||||
|
} else {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? next : prev
|
||||||
|
})
|
||||||
|
}, [libraries])
|
||||||
|
|
||||||
|
const chatLibrarySlug = activeSessionId ? (chatLibraryBySession[activeSessionId] || null) : null
|
||||||
|
|
||||||
|
const chatLibrary = useMemo(() => {
|
||||||
|
return libraries.find(lib => lib.slug === chatLibrarySlug) || null
|
||||||
|
}, [chatLibrarySlug, libraries])
|
||||||
|
|
||||||
|
const chatLibraryHasActiveJob = useMemo(() => {
|
||||||
|
if (!chatLibrarySlug) return false
|
||||||
|
return libraryJobs.some(job => job.slug === chatLibrarySlug && libraryJobIsActive(job))
|
||||||
|
}, [chatLibrarySlug, libraryJobs])
|
||||||
|
|
||||||
|
const chatLibraryStatusSuffix = useMemo(() => {
|
||||||
|
return getLibraryStatusSuffix(chatLibrary, chatLibraryHasActiveJob)
|
||||||
|
}, [chatLibrary, chatLibraryHasActiveJob])
|
||||||
|
|
||||||
|
function getChatLibrarySlugForSession(sessionId) {
|
||||||
|
if (!sessionId) return null
|
||||||
|
return chatLibraryBySession[sessionId] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChatLibraryForSession(sessionId) {
|
||||||
|
const slug = getChatLibrarySlugForSession(sessionId)
|
||||||
|
if (!slug) return null
|
||||||
|
return libraries.find(lib => lib.slug === slug) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLibrarySyncing(slug) {
|
||||||
|
if (!slug) return false
|
||||||
|
return libraryJobs.some(job => job.slug === slug && libraryJobIsActive(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setChatLibraryForSession(sessionId, slug) {
|
||||||
|
if (!sessionId) return
|
||||||
|
setChatLibraryBySession(prev => {
|
||||||
|
const next = { ...(prev || {}) }
|
||||||
|
if (slug) {
|
||||||
|
next[sessionId] = slug
|
||||||
|
} else {
|
||||||
|
delete next[sessionId]
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLibraryFromChatSelections(slug) {
|
||||||
|
if (!slug) return
|
||||||
|
setChatLibraryBySession(prev => {
|
||||||
|
let changed = false
|
||||||
|
const next = {}
|
||||||
|
for (const [sessionId, librarySlug] of Object.entries(prev || {})) {
|
||||||
|
if (librarySlug === slug) {
|
||||||
|
changed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next[sessionId] = librarySlug
|
||||||
|
}
|
||||||
|
return changed ? next : prev
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearChatLibrarySelections() {
|
||||||
|
setChatLibraryBySession({})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatLibrary,
|
||||||
|
chatLibraryBySession,
|
||||||
|
chatLibrarySlug,
|
||||||
|
chatLibraryStatusSuffix,
|
||||||
|
clearChatLibrarySelections,
|
||||||
|
getChatLibraryForSession,
|
||||||
|
isLibrarySyncing,
|
||||||
|
removeLibraryFromChatSelections,
|
||||||
|
setChatLibraryForSession,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user