diff --git a/src/App.jsx b/src/App.jsx
index e09a626..d120fe0 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -3,6 +3,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're
import { flushSync } from 'react-dom';
import TextareaAutosize from 'react-textarea-autosize';
import AssistantMessageContent from './AssistantMessageContent'
+import ChatDatabasePicker from './ChatDatabasePicker'
import LibraryManager from './LibraryManager'
import { SettingsPanel, SettingsSidebar } from './SettingsPanels'
import { applyColorScheme } from './colorSchemes'
@@ -23,7 +24,6 @@ import {
import {
AUDIO_RECORDING_TICK_MS,
BOTTOM_EPSILON,
- CHAT_LIBRARY_MAP_KEY,
DEFAULT_BACKEND_API_URL,
MAX_AUDIO_RECORDING_MS,
MAX_IMAGE_ATTACHMENT_BYTES,
@@ -52,6 +52,7 @@ import {
stopMediaStream,
supportsAudioInputCapture,
} from './audioInput'
+import { useChatLibrarySelection } from './useChatLibrarySelection'
function appendTranscriptToComposer(currentInput, transcript) {
const nextTranscript = String(transcript || '').trim()
diff --git a/src/ChatDatabasePicker.jsx b/src/ChatDatabasePicker.jsx
new file mode 100644
index 0000000..f26aedc
--- /dev/null
+++ b/src/ChatDatabasePicker.jsx
@@ -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 (
+
+
+ {isDbPickerOpen && (
+
+
+ {libraries.length === 0 ? (
+
No databases yet.
+ ) : (
+ 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 (
+
+ )
+ })
+ )}
+
+ )}
+
+ )
+}
diff --git a/src/useChatLibrarySelection.js b/src/useChatLibrarySelection.js
new file mode 100644
index 0000000..6f023bf
--- /dev/null
+++ b/src/useChatLibrarySelection.js
@@ -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,
+ }
+}