2025-10-04 21:21:42 +00:00
|
|
|
|
/*
|
|
|
|
|
|
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
|
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
#
|
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
|
#
|
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
#
|
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
*/
|
2025-10-05 22:27:47 +00:00
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
2025-10-04 21:21:42 +00:00
|
|
|
|
import styles from '@/styles/Sidebar.module.css';
|
|
|
|
|
|
|
|
|
|
|
|
interface Model {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ChatMetadata {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface SidebarProps {
|
|
|
|
|
|
showIngestion: boolean;
|
|
|
|
|
|
setShowIngestion: (value: boolean) => void;
|
|
|
|
|
|
refreshTrigger?: number;
|
|
|
|
|
|
currentChatId: string | null;
|
|
|
|
|
|
onChatChange: (chatId: string) => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default function Sidebar({
|
|
|
|
|
|
showIngestion,
|
|
|
|
|
|
setShowIngestion,
|
|
|
|
|
|
refreshTrigger = 0,
|
|
|
|
|
|
currentChatId,
|
|
|
|
|
|
onChatChange
|
|
|
|
|
|
}: SidebarProps) {
|
|
|
|
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
|
|
|
|
const [isClosing, setIsClosing] = useState(false);
|
|
|
|
|
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(["config", "history"]));
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
const [availableSources, setAvailableSources] = useState<string[]>([]);
|
|
|
|
|
|
const [selectedSources, setSelectedSources] = useState<string[]>([]);
|
|
|
|
|
|
const [selectedModel, setSelectedModel] = useState<string>("");
|
|
|
|
|
|
const [isLoadingSources, setIsLoadingSources] = useState(false);
|
|
|
|
|
|
const [availableModels, setAvailableModels] = useState<Model[]>([]);
|
|
|
|
|
|
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
|
|
|
|
const [chats, setChats] = useState<string[]>([]);
|
|
|
|
|
|
const [isLoadingChats, setIsLoadingChats] = useState(false);
|
|
|
|
|
|
const [chatMetadata, setChatMetadata] = useState<Record<string, ChatMetadata>>({});
|
|
|
|
|
|
|
|
|
|
|
|
// Add ref for chat list
|
|
|
|
|
|
const chatListRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// Load initial configuration
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadInitialConfig = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
// Get selected model
|
|
|
|
|
|
const modelResponse = await fetch("/api/selected_model");
|
|
|
|
|
|
if (modelResponse.ok) {
|
|
|
|
|
|
const { model } = await modelResponse.json();
|
|
|
|
|
|
setSelectedModel(model);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get selected sources
|
|
|
|
|
|
const sourcesResponse = await fetch("/api/selected_sources");
|
|
|
|
|
|
if (sourcesResponse.ok) {
|
|
|
|
|
|
const { sources } = await sourcesResponse.json();
|
|
|
|
|
|
setSelectedSources(sources);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get available models
|
|
|
|
|
|
await fetchAvailableModels();
|
|
|
|
|
|
|
|
|
|
|
|
// Get sources
|
|
|
|
|
|
await fetchSources();
|
|
|
|
|
|
|
|
|
|
|
|
// Get chats if history section is expanded (which it is by default)
|
|
|
|
|
|
if (expandedSections.has('history')) {
|
|
|
|
|
|
await fetchChats();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error loading initial config:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadInitialConfig();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch available models
|
|
|
|
|
|
const fetchAvailableModels = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setIsLoadingModels(true);
|
|
|
|
|
|
const response = await fetch("/api/available_models");
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
|
console.error(`Error fetching available models: ${response.status} - ${errorText}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
const models = data.models.map((modelId: string) => ({
|
|
|
|
|
|
id: modelId,
|
|
|
|
|
|
name: modelId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
|
|
|
|
|
}));
|
|
|
|
|
|
setAvailableModels(models);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error fetching available models:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoadingModels(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch available sources
|
2025-10-05 22:27:47 +00:00
|
|
|
|
const fetchSources = useCallback(async () => {
|
2025-10-04 21:21:42 +00:00
|
|
|
|
try {
|
|
|
|
|
|
setIsLoadingSources(true);
|
|
|
|
|
|
console.log("Fetching sources...");
|
|
|
|
|
|
const response = await fetch("/api/sources");
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
|
console.error(`Error fetching sources: ${response.status} - ${errorText}`);
|
|
|
|
|
|
setAvailableSources([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log("Sources fetched:", data.sources);
|
|
|
|
|
|
setAvailableSources(data.sources || []);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error fetching sources:", error);
|
|
|
|
|
|
setAvailableSources([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoadingSources(false);
|
|
|
|
|
|
}
|
2025-10-05 22:27:47 +00:00
|
|
|
|
}, []);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
// Get sources on initial load and when the context section is expanded
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (expandedSections.has('context')) {
|
|
|
|
|
|
fetchSources();
|
|
|
|
|
|
}
|
2025-10-05 22:27:47 +00:00
|
|
|
|
}, [expandedSections.has('context'), fetchSources]);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
// Refresh sources when refreshTrigger changes (document ingestion)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (refreshTrigger > 0) { // Only refresh if not the initial render
|
|
|
|
|
|
fetchSources();
|
|
|
|
|
|
}
|
2025-10-05 22:27:47 +00:00
|
|
|
|
}, [refreshTrigger, fetchSources]);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
// Add function to fetch chat metadata
|
2025-10-06 12:57:08 +00:00
|
|
|
|
const fetchChatMetadata = useCallback(async (chatId: string) => {
|
2025-10-04 21:21:42 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/chat/${chatId}/metadata`);
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const metadata = await response.json();
|
|
|
|
|
|
setChatMetadata(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[chatId]: metadata
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`Error fetching metadata for chat ${chatId}:`, error);
|
|
|
|
|
|
}
|
2025-10-06 12:57:08 +00:00
|
|
|
|
}, []);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
// Update fetchChats to also fetch metadata
|
2025-10-05 22:27:47 +00:00
|
|
|
|
const fetchChats = useCallback(async () => {
|
2025-10-04 21:21:42 +00:00
|
|
|
|
try {
|
|
|
|
|
|
console.log("fetchChats: Starting to fetch chats...");
|
|
|
|
|
|
setIsLoadingChats(true);
|
|
|
|
|
|
const response = await fetch("/api/chats");
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log("fetchChats: Received chats:", data.chats);
|
|
|
|
|
|
setChats(data.chats);
|
|
|
|
|
|
// Fetch metadata for each chat
|
|
|
|
|
|
await Promise.all(data.chats.map(fetchChatMetadata));
|
|
|
|
|
|
console.log("fetchChats: Completed fetching all chat metadata");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("fetchChats: Failed to fetch chats, status:", response.status);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error fetching chats:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoadingChats(false);
|
|
|
|
|
|
}
|
2025-10-06 12:57:08 +00:00
|
|
|
|
}, [fetchChatMetadata]);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
// Fetch chats when history section is expanded
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (expandedSections.has('history')) {
|
|
|
|
|
|
fetchChats();
|
|
|
|
|
|
}
|
2025-10-05 22:27:47 +00:00
|
|
|
|
}, [expandedSections.has('history'), fetchChats]);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update highlight position when currentChatId changes
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (currentChatId && chatListRef.current) {
|
|
|
|
|
|
const activeChat = chatListRef.current.querySelector(`.${styles.active}`) as HTMLElement;
|
|
|
|
|
|
if (activeChat) {
|
|
|
|
|
|
const offset = activeChat.offsetTop;
|
|
|
|
|
|
chatListRef.current.style.setProperty('--highlight-offset', `${offset}px`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [currentChatId, chats]);
|
|
|
|
|
|
|
|
|
|
|
|
// Add new effect to handle initial position and chat list loading
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (chatListRef.current && currentChatId && chats.length > 0) {
|
|
|
|
|
|
// Small delay to ensure DOM is ready
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const activeChat = chatListRef.current?.querySelector(`.${styles.active}`) as HTMLElement;
|
|
|
|
|
|
if (activeChat) {
|
|
|
|
|
|
const offset = activeChat.offsetTop;
|
|
|
|
|
|
chatListRef.current?.style.setProperty('--highlight-offset', `${offset}px`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 50);
|
|
|
|
|
|
}
|
2025-10-05 22:27:47 +00:00
|
|
|
|
}, [currentChatId, chats.length]);
|
2025-10-04 21:21:42 +00:00
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
setIsClosing(true);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setIsVisible(false);
|
|
|
|
|
|
setIsClosing(false);
|
|
|
|
|
|
}, 500); // Match the new animation duration
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleSidebar = () => {
|
|
|
|
|
|
if (!isVisible) {
|
|
|
|
|
|
setIsVisible(true);
|
|
|
|
|
|
setIsClosing(false);
|
|
|
|
|
|
fetchSources();
|
|
|
|
|
|
// Also fetch chats when sidebar opens
|
|
|
|
|
|
if (expandedSections.has('history')) {
|
|
|
|
|
|
fetchChats();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleSection = (section: string) => {
|
|
|
|
|
|
const newExpandedSections = new Set(expandedSections);
|
|
|
|
|
|
if (newExpandedSections.has(section)) {
|
|
|
|
|
|
newExpandedSections.delete(section);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newExpandedSections.add(section);
|
|
|
|
|
|
// Get sources when context section is expanded
|
|
|
|
|
|
if (section === 'context') {
|
|
|
|
|
|
fetchSources();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setExpandedSections(newExpandedSections);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const isSectionExpanded = (section: string) => {
|
|
|
|
|
|
return expandedSections.has(section);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSourceToggle = async (source: string) => {
|
|
|
|
|
|
let newSelectedSources: string[];
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedSources.includes(source)) {
|
|
|
|
|
|
// Remove source if already selected
|
|
|
|
|
|
newSelectedSources = selectedSources.filter(s => s !== source);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Add source if not selected
|
|
|
|
|
|
newSelectedSources = [...selectedSources, source];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedSources(newSelectedSources);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch("/api/selected_sources", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify(newSelectedSources)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
console.error("Failed to update selected sources");
|
|
|
|
|
|
// Revert the local state if the update failed
|
|
|
|
|
|
setSelectedSources(selectedSources);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error updating selected sources:", error);
|
|
|
|
|
|
// Revert the local state if the update failed
|
|
|
|
|
|
setSelectedSources(selectedSources);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleChatSelect = async (chatId: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await onChatChange(chatId);
|
|
|
|
|
|
|
|
|
|
|
|
// Close sidebar on mobile after selection
|
|
|
|
|
|
if (window.innerWidth < 768) {
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error selecting chat:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRenameChat = async (chatId: string, currentName: string) => {
|
|
|
|
|
|
const newName = prompt("Enter new chat name:", currentName);
|
|
|
|
|
|
if (newName && newName.trim() && newName !== currentName) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch("/api/chat/rename", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({ chat_id: chatId, new_name: newName.trim() })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
console.error("Failed to rename chat");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch updated metadata for the renamed chat
|
|
|
|
|
|
await fetchChatMetadata(chatId);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error renaming chat:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteChat = async (chatId: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Delete the chat
|
|
|
|
|
|
const response = await fetch(`/api/chat/${chatId}`, {
|
|
|
|
|
|
method: "DELETE"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
console.error("Failed to delete chat");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Refresh chat list
|
|
|
|
|
|
await fetchChats();
|
|
|
|
|
|
|
|
|
|
|
|
// If we deleted the current chat
|
|
|
|
|
|
if (currentChatId === chatId) {
|
|
|
|
|
|
// Get updated list of chats
|
|
|
|
|
|
const chatsResponse = await fetch("/api/chats");
|
|
|
|
|
|
const { chats: remainingChats } = await chatsResponse.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (remainingChats.length > 0) {
|
|
|
|
|
|
// Switch to another chat
|
|
|
|
|
|
await onChatChange(remainingChats[0]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No chats left, create a new one
|
|
|
|
|
|
await handleNewChat();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error deleting chat:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNewChat = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("handleNewChat: Starting new chat creation...");
|
|
|
|
|
|
|
|
|
|
|
|
// Create new chat using backend endpoint
|
|
|
|
|
|
const response = await fetch("/api/chat/new", {
|
|
|
|
|
|
method: "POST"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
console.error("Failed to create new chat");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log("handleNewChat: Created new chat:", data.chat_id);
|
|
|
|
|
|
|
|
|
|
|
|
// First, refresh the chat list to ensure the new chat is available
|
|
|
|
|
|
console.log("handleNewChat: Refreshing chat list...");
|
|
|
|
|
|
await fetchChats();
|
|
|
|
|
|
console.log("handleNewChat: Chat list refreshed");
|
|
|
|
|
|
|
|
|
|
|
|
// Then change to the new chat
|
|
|
|
|
|
await onChatChange(data.chat_id);
|
|
|
|
|
|
console.log("handleNewChat: Changed to new chat");
|
|
|
|
|
|
|
|
|
|
|
|
// Close sidebar on mobile
|
|
|
|
|
|
if (window.innerWidth < 768) {
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add a small delay to ensure the DOM has updated, then trigger highlight animation
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (chatListRef.current) {
|
|
|
|
|
|
const activeChat = chatListRef.current.querySelector(`.${styles.active}`) as HTMLElement;
|
|
|
|
|
|
if (activeChat) {
|
|
|
|
|
|
const offset = activeChat.offsetTop;
|
|
|
|
|
|
chatListRef.current.style.setProperty('--highlight-offset', `${offset}px`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100); // Increased delay for more reliability
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error creating new chat:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClearAllChats = async () => {
|
|
|
|
|
|
// Show confirmation dialog
|
|
|
|
|
|
const confirmClear = window.confirm(
|
|
|
|
|
|
`Are you sure you want to clear all ${chats.length} chat conversations? This action cannot be undone.`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmClear) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch("/api/chats/clear", {
|
|
|
|
|
|
method: "DELETE"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
console.error("Failed to clear all chats");
|
|
|
|
|
|
alert("Failed to clear chats. Please try again.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
// Switch to the new chat created by the backend
|
|
|
|
|
|
await onChatChange(data.new_chat_id);
|
|
|
|
|
|
|
|
|
|
|
|
// Refresh chat list
|
|
|
|
|
|
await fetchChats();
|
|
|
|
|
|
|
|
|
|
|
|
// Close sidebar on mobile
|
|
|
|
|
|
if (window.innerWidth < 768) {
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`Successfully cleared ${data.cleared_count} chats`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error clearing all chats:", error);
|
|
|
|
|
|
alert("An error occurred while clearing chats. Please try again.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleModelChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
|
|
|
|
const newModel = event.target.value;
|
|
|
|
|
|
const newModelLower = newModel.toLowerCase();
|
|
|
|
|
|
setSelectedModel(newModel);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("Updating selected model to:", newModel);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch("/api/selected_model", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({ model: newModel })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
console.error("Failed to update selected model");
|
|
|
|
|
|
// Revert the local state if the update failed
|
|
|
|
|
|
setSelectedModel(selectedModel);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error updating selected model:", error);
|
|
|
|
|
|
// Revert the local state if the update failed
|
|
|
|
|
|
setSelectedModel(selectedModel);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={`${styles.toggleSidebarButton} ${isVisible && !isClosing ? styles.active : ''}`}
|
|
|
|
|
|
onClick={toggleSidebar}
|
|
|
|
|
|
>
|
|
|
|
|
|
☰
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{isVisible && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`${styles.sidebarOverlay} ${isClosing ? styles.closing : ''}`}
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className={`${styles.sidebar} ${isClosing ? styles.closing : ''}`}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.closeSidebarButton}
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div className={styles.sidebarHeader}>
|
|
|
|
|
|
<h2 className={styles.title}>Spark Chat</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Model Selection */}
|
|
|
|
|
|
<div className={styles.sidebarSection}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={styles.sectionHeader}
|
|
|
|
|
|
onClick={() => toggleSection('model')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h3>Model</h3>
|
|
|
|
|
|
<span className={isSectionExpanded('model') ? styles.arrowUp : styles.arrowDown}>▼</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={`${styles.sectionContent} ${isSectionExpanded('model') ? styles.expanded : ''}`}>
|
|
|
|
|
|
<div className={styles.configItem}>
|
|
|
|
|
|
<label htmlFor="model-select">Select Supervisor Model</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
id="model-select"
|
|
|
|
|
|
className={styles.modelSelect}
|
|
|
|
|
|
value={selectedModel}
|
|
|
|
|
|
onChange={handleModelChange}
|
|
|
|
|
|
disabled={isLoadingModels}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isLoadingModels ? (
|
|
|
|
|
|
<option value="">Loading models...</option>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
availableModels.map(model => (
|
|
|
|
|
|
<option key={model.id} value={model.id}>
|
|
|
|
|
|
{model.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Context */}
|
|
|
|
|
|
<div className={styles.sidebarSection}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={styles.sectionHeader}
|
|
|
|
|
|
onClick={() => toggleSection('context')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h3>Context</h3>
|
|
|
|
|
|
<span className={isSectionExpanded('context') ? styles.arrowUp : styles.arrowDown}>▼</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={`${styles.sectionContent} ${isSectionExpanded('context') ? styles.expanded : ''}`}>
|
|
|
|
|
|
<div className={styles.configItem}>
|
|
|
|
|
|
<label>Select Sources</label>
|
|
|
|
|
|
<div className={styles.sourcesContainer}>
|
|
|
|
|
|
{availableSources.length === 0 ? (
|
|
|
|
|
|
<div className={styles.noSources}>No sources available</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
availableSources.map(source => (
|
|
|
|
|
|
<div key={source} className={styles.sourceItem}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
id={`source-${source}`}
|
|
|
|
|
|
checked={selectedSources.includes(source)}
|
|
|
|
|
|
onChange={() => handleSourceToggle(source)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label htmlFor={`source-${source}`}>{source}</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-05 22:27:47 +00:00
|
|
|
|
<div className={styles.buttonGroup}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.uploadDocumentsButton}
|
|
|
|
|
|
onClick={() => setShowIngestion(true)}
|
|
|
|
|
|
title="Upload Documents"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
width="16"
|
|
|
|
|
|
height="16"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Upload Documents
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.refreshButton}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
fetchSources();
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={isLoadingSources}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isLoadingSources ? "Loading..." : "Refresh Sources"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-10-04 21:21:42 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Chat History */}
|
|
|
|
|
|
<div className={styles.sidebarSection}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={styles.sectionHeader}
|
|
|
|
|
|
onClick={() => toggleSection('history')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h3>Chat History</h3>
|
|
|
|
|
|
<span className={isSectionExpanded('history') ? styles.arrowUp : styles.arrowDown}>▼</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={`${styles.sectionContent} ${isSectionExpanded('history') ? styles.expanded : ''}`}>
|
|
|
|
|
|
<div className={styles.chatList} ref={chatListRef}>
|
|
|
|
|
|
{isLoadingChats ? (
|
|
|
|
|
|
<div className={styles.loadingText}>Loading chats...</div>
|
|
|
|
|
|
) : chats.length === 0 ? (
|
|
|
|
|
|
<div className={styles.noChatText}>No previous chats</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
chats.map((chatId) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={chatId}
|
|
|
|
|
|
className={`${styles.chatItem} ${currentChatId === chatId ? styles.active : ''}`}
|
|
|
|
|
|
onClick={() => handleChatSelect(chatId)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className={styles.chatName}>
|
|
|
|
|
|
{chatMetadata[chatId]?.name || chatId.slice(0, 8)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.chatActions}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.chatActionButton}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
handleRenameChat(chatId, chatMetadata[chatId]?.name || `Chat ${chatId.slice(0, 8)}`);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
✎
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.chatActionButton}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
handleDeleteChat(chatId);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Chat Action Buttons at bottom */}
|
|
|
|
|
|
<div className={styles.chatButtonsContainer}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.newChatButton}
|
|
|
|
|
|
onClick={handleNewChat}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
>
|
|
|
|
|
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
|
|
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
New Chat
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={styles.clearChatsButton}
|
|
|
|
|
|
onClick={handleClearAllChats}
|
|
|
|
|
|
disabled={chats.length === 0}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path d="M3 6h18" />
|
|
|
|
|
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
|
|
|
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
Clear All
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|