From a2b56a02e495533ec6931981d4add9f7fb389e51 Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Mon, 21 Oct 2024 16:34:06 -0400 Subject: [PATCH] Demo: RAG/Docling/llama-index Signed-off-by: Brent Salisbury --- .../[collectionName]/delete/route.tsx | 31 + .../[collectionName]/documents/file/route.tsx | 47 ++ .../[collectionName]/documents/url/route.tsx | 31 + .../[collectionName]/query/route.tsx | 43 ++ .../playground/ragchat/collections/route.ts | 31 + src/app/playground/ragchat/page.tsx | 619 ++++++++++++++++++ src/app/playground/ragchat/ragchat.module.css | 176 +++++ src/components/AppLayout.tsx | 3 +- 8 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 src/app/api/playground/ragchat/collections/[collectionName]/delete/route.tsx create mode 100644 src/app/api/playground/ragchat/collections/[collectionName]/documents/file/route.tsx create mode 100644 src/app/api/playground/ragchat/collections/[collectionName]/documents/url/route.tsx create mode 100644 src/app/api/playground/ragchat/collections/[collectionName]/query/route.tsx create mode 100644 src/app/api/playground/ragchat/collections/route.ts create mode 100644 src/app/playground/ragchat/page.tsx create mode 100644 src/app/playground/ragchat/ragchat.module.css diff --git a/src/app/api/playground/ragchat/collections/[collectionName]/delete/route.tsx b/src/app/api/playground/ragchat/collections/[collectionName]/delete/route.tsx new file mode 100644 index 00000000..8b8f7fd9 --- /dev/null +++ b/src/app/api/playground/ragchat/collections/[collectionName]/delete/route.tsx @@ -0,0 +1,31 @@ +'use server'; + +import { NextRequest, NextResponse } from 'next/server'; +import fetch from 'node-fetch'; + +export async function DELETE(req: NextRequest, { params }: { params: { collectionName: string } }) { + const { collectionName } = params; + + try { + console.log(`Deleting collection: ${collectionName}`); + + // Make the API request to the backend to delete the collection + const response = await fetch(`http://127.0.0.1:8000/collections/${encodeURIComponent(collectionName)}`, { + method: 'DELETE' + }); + + // Check if the response was successful + if (!response.ok) { + const errorText = await response.text(); + console.error(`Failed to delete collection: ${errorText}`); + throw new Error(`Failed to delete collection: ${errorText}`); + } + + // Return a success response to the client + console.log(`Collection ${collectionName} deleted successfully.`); + return NextResponse.json({ message: `Collection ${collectionName} deleted successfully.` }, { status: 200 }); + } catch (error: any) { + console.error('Error deleting collection:', error.message); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/playground/ragchat/collections/[collectionName]/documents/file/route.tsx b/src/app/api/playground/ragchat/collections/[collectionName]/documents/file/route.tsx new file mode 100644 index 00000000..bb67d5d0 --- /dev/null +++ b/src/app/api/playground/ragchat/collections/[collectionName]/documents/file/route.tsx @@ -0,0 +1,47 @@ +// src/app/api/playground/ragchat/collections/[collectionName]/documents/file/route.ts +'use server'; + +import { NextRequest, NextResponse } from 'next/server'; +import fetch from 'node-fetch'; +import FormData from 'form-data'; + +export async function POST(req: NextRequest, { params }: { params: { collectionName: string } }) { + const { collectionName } = params; + + try { + // Parse the form data from the incoming request + const formData = await req.formData(); + const file = formData.get('files') as File | null; + + if (!file) { + throw new Error('File is required for upload'); + } + + // Create FormData for the backend request + const backendFormData = new FormData(); + + // Convert the file to a Buffer for the Node.js environment + const buffer = Buffer.from(await file.arrayBuffer()); + + // Append the file buffer to FormData + backendFormData.append('file', buffer, file.name); + + // Send the file to the backend service + const backendResponse = await fetch(`http://127.0.0.1:8000/collections/${encodeURIComponent(collectionName)}/documents/file`, { + method: 'POST', + body: backendFormData, + headers: backendFormData.getHeaders() + }); + + const backendResponseText = await backendResponse.text(); + + if (!backendResponse.ok) { + throw new Error(`Failed to upload file to backend: ${backendResponseText}`); + } + + return NextResponse.json({ message: 'File uploaded successfully', data: backendResponseText }, { status: 200 }); + } catch (error: any) { + console.error('Error during file upload:', error.message); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/playground/ragchat/collections/[collectionName]/documents/url/route.tsx b/src/app/api/playground/ragchat/collections/[collectionName]/documents/url/route.tsx new file mode 100644 index 00000000..8a6e1fd4 --- /dev/null +++ b/src/app/api/playground/ragchat/collections/[collectionName]/documents/url/route.tsx @@ -0,0 +1,31 @@ +// src/app/api/playground/ragchat/collections/[collectionName]/documents/url/route.ts +`use server`; + +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(req: NextRequest, { params }: { params: { collectionName: string } }) { + const { collectionName } = params; + + try { + const { http_source } = await req.json(); + + const response = await fetch(`http://localhost:8000/collections/${encodeURIComponent(collectionName)}/documents/url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ http_source }) + }); + + const responseText = await response.text(); + + if (!response.ok) { + throw new Error(`Failed to upload URL: ${responseText}`); + } + + return NextResponse.json({ message: 'URL uploaded successfully', data: responseText }, { status: 200 }); + } catch (error: any) { + console.error('Error uploading URL:', error.message); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/playground/ragchat/collections/[collectionName]/query/route.tsx b/src/app/api/playground/ragchat/collections/[collectionName]/query/route.tsx new file mode 100644 index 00000000..7588c56a --- /dev/null +++ b/src/app/api/playground/ragchat/collections/[collectionName]/query/route.tsx @@ -0,0 +1,43 @@ +// src/app/api/playground/ragchat/collections/[collectionName]/query/route.ts +'use server'; + +import { NextRequest, NextResponse } from 'next/server'; +import fetch from 'node-fetch'; + +export async function POST(req: NextRequest, { params }: { params: { collectionName: string } }) { + const { collectionName } = params; + + try { + const { question } = await req.json(); + + console.log(`Received question: ${question} for collection: ${collectionName}`); + + const response = await fetch(`http://127.0.0.1:8000/collections/${encodeURIComponent(collectionName)}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ question }) + }); + + // Check if the response was successful + if (!response.ok) { + const errorText = await response.text(); + console.error(`Failed to query collection: ${errorText}`); + throw new Error(`Failed to query collection: ${errorText}`); + } + + // Parse the backend response + const responseData = await response.json(); + console.log('Backend response data:', responseData); + + // Extract the 'answer' and 'sources' fields + const { answer, sources } = responseData; + + // Return the answer and sources to the client + return NextResponse.json({ answer, sources }, { status: 200 }); + } catch (error: any) { + console.error('Error querying collection:', error.message); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/playground/ragchat/collections/route.ts b/src/app/api/playground/ragchat/collections/route.ts new file mode 100644 index 00000000..5fc07245 --- /dev/null +++ b/src/app/api/playground/ragchat/collections/route.ts @@ -0,0 +1,31 @@ +// src/app/api/playground/ragchat/collections/route.ts +'use server'; + +import { NextRequest, NextResponse } from 'next/server'; +import fetch from 'node-fetch'; + +export async function GET(req: NextRequest) { + console.log('Received request to fetch collections'); + + try { + console.log('Making fetch call to backend service...'); + + const response = await fetch('http://127.0.0.1:8000/collections', { + method: 'GET', + headers: { + Accept: 'application/json' // Ensure Accept header is set properly + } + }); + + const rawText = await response.text(); + console.log('Raw response text from backend:', rawText); + + const data = JSON.parse(rawText); + console.log('Parsed collections data:', data); + + return NextResponse.json(data, { status: 200 }); + } catch (error: any) { + console.error('Error fetching collections:', error.message); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/playground/ragchat/page.tsx b/src/app/playground/ragchat/page.tsx new file mode 100644 index 00000000..9bd88d54 --- /dev/null +++ b/src/app/playground/ragchat/page.tsx @@ -0,0 +1,619 @@ +// src/app/playground/ragchat +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { AppLayout } from '@/components/AppLayout'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { Form, FormGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { Select } from '@patternfly/react-core/dist/dynamic/components/Select'; +import { SelectOption, SelectList } from '@patternfly/react-core/dist/dynamic/components/Select'; +import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; +import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; +import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core/dist/dynamic/components/ToggleGroup'; +import { + MultipleFileUpload, + MultipleFileUploadMain, + MultipleFileUploadStatus, + MultipleFileUploadStatusItem +} from '@patternfly/react-core/dist/dynamic/components/MultipleFileUpload'; +import { Modal } from '@patternfly/react-core/dist/dynamic/components/Modal'; +import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import UploadIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon'; +import UserIcon from '@patternfly/react-icons/dist/dynamic/icons/user-icon'; +import CopyIcon from '@patternfly/react-icons/dist/dynamic/icons/copy-icon'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBroom } from '@fortawesome/free-solid-svg-icons'; +import Image from 'next/image'; +import styles from './ragchat.module.css'; + +interface Message { + text: string; + isUser: boolean; +} + +interface ReadFile { + fileName: string; + data?: string; + loadResult?: 'danger' | 'success'; + loadError?: DOMException; +} + +const Page: React.FC = () => { + // State variables + const [question, setQuestion] = useState(''); + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isCollectionSelectOpen, setIsCollectionSelectOpen] = useState(false); + const [collections, setCollections] = useState([]); + const [selectedCollection, setSelectedCollection] = useState(null); + const [uploadCollectionName, setUploadCollectionName] = useState(''); // Collection name input for file upload + const [uploadURL, setUploadURL] = useState(''); + const [ingestMethod, setIngestMethod] = useState('url'); + const messagesContainerRef = useRef(null); + + // File upload state + const [currentFiles, setCurrentFiles] = useState([]); + const [readFileData, setReadFileData] = useState([]); + const [showStatus, setShowStatus] = useState(false); + const [statusIcon, setStatusIcon] = useState<'inProgress' | 'success' | 'danger'>('inProgress'); + const [modalText, setModalText] = useState(''); + const [urlCollectionName, setUrlCollectionName] = useState(''); // Collection name for URL upload + const [fileCollectionName, setFileCollectionName] = useState(''); // Collection name for file upload + + useEffect(() => { + let isMounted = true; + let timeoutId: NodeJS.Timeout; + + fetchCollections(); + + return () => { + isMounted = false; + clearTimeout(timeoutId); + }; + }, []); + + // Handlers for collection selection and toggle + const onCollectionToggleClick = () => { + setIsCollectionSelectOpen((prev) => !prev); + + // Force refresh collections when dropdown is opened + if (!isCollectionSelectOpen) { + console.log('Dropdown opened, fetching collections'); + fetchCollections(); + } + }; + + useEffect(() => { + if (currentFiles.length > 0) { + setShowStatus(true); + } else { + setShowStatus(false); + } + }, [currentFiles]); + + useEffect(() => { + if (readFileData.length < currentFiles.length) { + setStatusIcon('inProgress'); + } else if (readFileData.every((file) => file.loadResult === 'success')) { + setStatusIcon('success'); + } else { + setStatusIcon('danger'); + } + }, [readFileData, currentFiles]); + + // Handlers for collection selection + // const onCollectionToggleClick = () => { + // setIsCollectionSelectOpen(!isCollectionSelectOpen); + // }; + + // Fetch collections from the server + const fetchCollections = async () => { + console.log('Fetching collections...'); + try { + const response = await fetch('/api/playground/ragchat/collections'); + if (response.ok) { + const data = await response.json(); + setCollections(data.collections); + console.log('Collections fetched:', data.collections); + } else { + console.error('Failed to fetch collections:', await response.text()); + } + } catch (error) { + console.error('Error fetching collections:', error); + } + }; + const onCollectionSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + setSelectedCollection(String(value)); + setIsCollectionSelectOpen(false); + }; + + const collectionToggle = (toggleRef: React.Ref) => ( + + {selectedCollection ? selectedCollection : 'Select a collection'} + + ); + + const collectionItems = collections.map((collection) => ( + + {collection} + + )); + + // Handler for question input change + const handleQuestionChange = (_event: React.FormEvent, value: string) => { + setQuestion(value); + }; + + // Handler for file upload + // Inside Page.tsx + const handleFileUpload = async () => { + if (!uploadCollectionName || currentFiles.length === 0) { + alert('Please enter collection name and select at least one file'); + return; + } + + const formData = new FormData(); + currentFiles.forEach((file) => { + formData.append('files', file); + }); + + try { + const response = await fetch(`/api/playground/ragchat/collections/${encodeURIComponent(uploadCollectionName)}/documents/file`, { + method: 'POST', + body: formData + }); + + const responseText = await response.text(); + console.log('File upload response:', responseText); + + if (response.ok) { + console.log('File uploaded successfully'); + if (!collections.includes(uploadCollectionName)) { + setCollections([...collections, uploadCollectionName]); + } + // Clear the files after successful upload + setCurrentFiles([]); + setReadFileData([]); + setShowStatus(false); + setStatusIcon('inProgress'); + } else { + console.error('Failed to upload file:', responseText); + alert(`Failed to upload file: ${responseText}`); + } + } catch (error) { + console.error('Error during file upload:', error); + alert('An error occurred during file upload.'); + } + }; + + // Handler for URL upload + const handleURLUpload = async () => { + if (!uploadCollectionName || !uploadURL) { + alert('Please enter collection name and URL'); + return; + } + + try { + const response = await fetch(`/api/playground/ragchat/collections/${encodeURIComponent(uploadCollectionName)}/documents/url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + http_source: { + url: uploadURL, + headers: {} + } + }) + }); + + const responseData = await response.json(); + console.log('URL upload response:', responseData); + + if (response.ok) { + console.log('URL uploaded successfully'); + if (!collections.includes(uploadCollectionName)) { + setCollections([...collections, uploadCollectionName]); + } + // Optionally, reset the upload URL input + setUploadURL(''); + } else { + console.error('Failed to upload URL:', responseData.error); + alert(`Failed to upload URL: ${responseData.error}`); + } + } catch (error) { + console.error('Error during URL upload:', error); + alert('An error occurred during URL upload.'); + } + }; + + // Handler for deleting a collection + const handleDeleteCollection = async () => { + if (!selectedCollection) return; + + try { + const response = await fetch(`/api/playground/ragchat/collections/${encodeURIComponent(selectedCollection)}/delete`, { + method: 'DELETE' + }); + + if (response.ok) { + setCollections(collections.filter((collection) => collection !== selectedCollection)); + setSelectedCollection(null); + console.log('Collection deleted successfully'); + } else { + console.error('Failed to delete collection:', await response.text()); + alert('Failed to delete collection.'); + } + } catch (error) { + console.error('Error during collection deletion:', error); + alert('An error occurred during collection deletion.'); + } + }; + + // Handler for submitting a query + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!question.trim() || !selectedCollection) return; + + setMessages((messages) => [...messages, { text: question, isUser: true }]); + setQuestion(''); + + setIsLoading(true); + try { + const response = await fetch(`/api/playground/ragchat/collections/${encodeURIComponent(selectedCollection)}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ question }) + }); + + if (!response.ok) { + const errorText = await response.text(); + setMessages((messages) => [...messages, { text: 'Failed to fetch response from the server.', isUser: false }]); + setIsLoading(false); + return; + } + + const data = await response.json(); + const answer = data.answer || ''; + const sources = data.sources || []; + + console.log('Answer:', answer); + console.log('Sources:', sources); + + setMessages((messages) => [...messages, { text: answer, isUser: false, sources }]); + setIsLoading(false); + } catch (error) { + console.error('Error during question submission:', error); + setIsLoading(false); + alert('An error occurred during question submission.'); + } + }; + + // Scroll to the bottom of the messages when new messages are added + useEffect(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }, [messages]); + + // Handler for copying text to clipboard + const handleCopyToClipboard = (text: string) => { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard + .writeText(text) + .then(() => { + console.log('Text copied to clipboard'); + }) + .catch((err) => { + console.error('Could not copy text: ', err); + }); + } else { + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + console.log('Text copied to clipboard'); + } catch (err) { + console.error('Could not copy text: ', err); + } + document.body.removeChild(textArea); + } + }; + + // Handler for cleaning up messages + const handleCleanup = () => { + setMessages([]); + }; + + // Handler for changing ingest method + const handleIngestMethodChange = (event: React.MouseEvent) => { + const id = event.currentTarget.id; + setIngestMethod(id === 'toggle-group-single-file' ? 'file' : 'url'); + }; + + // Functions for file upload + const removeFiles = (namesOfFilesToRemove: string[]) => { + const newCurrentFiles = currentFiles.filter((file) => !namesOfFilesToRemove.includes(file.name)); + const newReadFiles = readFileData.filter((file) => !namesOfFilesToRemove.includes(file.fileName)); + setCurrentFiles(newCurrentFiles); + setReadFileData(newReadFiles); + }; + + const handleFileDrop = (_event: any, droppedFiles: File[]) => { + const currentFileNames = currentFiles.map((file) => file.name); + const reUploads = droppedFiles.filter((file) => currentFileNames.includes(file.name)); + + const newFiles = [ + ...currentFiles.filter((file) => !reUploads.includes(file)), + ...droppedFiles.filter((file) => !currentFileNames.includes(file.name)) + ]; + setCurrentFiles(newFiles); + }; + + const handleReadSuccess = (data: string, file: File) => { + setReadFileData((prevReadFiles) => { + const existingFile = prevReadFiles.find((readFile) => readFile.fileName === file.name); + if (existingFile) { + return prevReadFiles; + } + return [...prevReadFiles, { data, fileName: file.name, loadResult: 'success' }]; + }); + }; + + const handleReadFail = (error: DOMException, file: File) => { + setReadFileData((prevReadFiles) => { + const existingFile = prevReadFiles.find((readFile) => readFile.fileName === file.name); + if (existingFile) { + return prevReadFiles; + } + return [...prevReadFiles, { loadError: error, fileName: file.name, loadResult: 'danger' }]; + }); + }; + + const handleDropRejected = (fileRejections: any[]) => { + console.warn('Files rejected:', fileRejections); + if (fileRejections.length === 1) { + setModalText(`${fileRejections[0].file.name} is not an accepted file type.`); + } else { + const rejectedMessages = fileRejections.reduce((acc, fileRejection) => (acc += `${fileRejection.file.name}, `), ''); + setModalText(`${rejectedMessages} are not accepted file types.`); + } + }; + + const createHelperText = (file: File) => { + const fileResult = readFileData.find((readFile) => readFile.fileName === file.name); + if (fileResult?.loadError) { + return ( + + {fileResult.loadError.toString()} + + ); + } + }; + + const successfullyReadFileCount = readFileData.filter((fileData) => fileData.loadResult === 'success').length; + + // Format metadata for display + const formatMetadata = (metadata: any) => { + return Object.keys(metadata) + .map((key) => `${key}: ${metadata[key]}`) + .join(', '); + }; + + return ( + +
+ {/* Ingest Data Method Toggle */} +
+ Ingest Data Method + + + + +
+ + {/* Collection Name Input for File/URL Upload */} + + setUploadCollectionName(value)} + placeholder="Enter collection name for upload" + /> + + + {/* Ingest Data Section */} + {ingestMethod === 'file' ? ( + <> + {/* File Upload Dropzone */} +
+ + } + titleText="Drag and drop files here" + titleTextSeparator="or" + infoText="Accepted file types: PDF, TXT, DOC, DOCX" + /> + {showStatus && ( + + {currentFiles.map((file) => ( + removeFiles([file.name])} + onReadSuccess={handleReadSuccess} + onReadFail={handleReadFail} + progressHelperText={createHelperText(file)} + /> + ))} + + )} + setModalText('')} + actions={[ + + ]} + > +

{modalText}

+
+
+
+ {/* Upload Files Button */} + + + ) : ( + /* URL Upload Section */ +
+ + setUploadURL(value)} + placeholder="Enter URL to Ingest" + /> + + {/* Made the Upload URL button full width */} + +
+ )} +
+ {/* Messages Container */} +
+ {messages.map((msg, index) => ( +
+ {msg.isUser ? ( + + ) : ( + Bot + )} +
+
+                  {msg.text}
+                
+ {!msg.isUser && msg.sources && ( +
+
+
Sources:
+ {msg.sources.map((source, i) => ( +
+

+ Source {i + 1}: {source.text} +

+

+ Metadata: {formatMetadata(source.metadata)} +

+
+ ))} +
+ )} +
+ {!msg.isUser && ( + + )} +
+ ))} + {isLoading && } +
+ + {/* Cleanup Button */} +
+ +
+ + {/* Chat Form */} +
+
+ {/* Collection Selector for Query Submission */} +
+ Select Collection + + {selectedCollection && ( + + )} +
+ {/* Question Input */} + + + + +
+
+
+
+ ); +}; + +export default Page; diff --git a/src/app/playground/ragchat/ragchat.module.css b/src/app/playground/ragchat/ragchat.module.css new file mode 100644 index 00000000..ef114ad8 --- /dev/null +++ b/src/app/playground/ragchat/ragchat.module.css @@ -0,0 +1,176 @@ +/* Chat Container */ +.chatContainer { + display: flex; + flex-direction: column; + justify-content: flex-start; + width: 100%; + height: 90vh; + background-color: #f7f7f8; + padding: 20px; + box-sizing: border-box; +} + +.modelAndUploadContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.selectDocumentContainer { + display: flex; + align-items: center; + gap: 10px; + margin-top: 10px; + margin-bottom: 10px; +} + +.fileUpload { + display: flex; + align-items: center; + margin-right: 40px; +} + +.fileUpload span { + margin-right: 10px; +} + +/* Model Selector */ +.modelSelector { + display: flex; + align-items: center; +} + +.modelSelectorLabel { + margin-right: 10px; + font-weight: bold; +} + +/* File Upload */ +.fileUpload input[type='file'] { + display: flex; + align-items: center; + margin-right: 40px; +} + +/* Chat Title */ +.chatTitle { + font-size: 1.5rem; + color: #333; + margin-bottom: 20px; + text-align: center; +} + +/* Messages Container */ +.messagesContainer { + flex-grow: 1; + overflow-y: auto; + padding: 20px; + border: 1px solid #e0e0e0; + background-color: white; +} + +/* Message Styling */ +.message { + display: flex; + align-items: flex-start; + margin-bottom: 20px; + font-size: 0.9rem; /* Smaller bot font size */ +} + +.message pre { + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + margin: 0; + flex-grow: 1; + font-size: inherit; /* Inherit the smaller font size */ +} + +/* User Message Styling */ +.chatQuestion { + justify-content: flex-start; + text-align: left; +} + +.userIcon { + margin-left: 10px; + margin-right: 20px; +} + +/* Bot Message Styling */ +.chatAnswer { + justify-content: flex-start; +} + +.botIcon { + margin-right: 10px; + margin-left: 0; +} + +/* Form Container within the Chat Window */ +.chatFormContainer { + display: flex; + align-items: center; + border-top: 1px solid #e0e0e0; + background-color: white; + padding: 10px; +} + +/* Form Styling */ +.chatForm { + display: flex; + width: 100%; + align-items: center; +} + +/* Input Fields Container */ +.inputFieldsContainer { + display: flex; + align-items: center; + flex-grow: 1; +} + +/* Input Fields */ +.inputFields { + display: flex; + flex-direction: column; + flex-grow: 1; + margin-right: 10px; +} + +.inputField { + margin-bottom: 10px; +} + +.sendButton { + display: flex; + align-items: center; + justify-content: center; + background-color: #007bff; + color: white; + border: none; + width: 50px; + height: 50px; + border-radius: 10px; + cursor: pointer; +} + +.sendButton:hover { + background-color: #0056b3; +} + +.spinner { + display: block; + margin: 20px auto; +} + +.boldLabel { + font-weight: bold; +} + +.cleanupButtonContainer { + display: flex; + justify-content: flex-end; + padding-right: 10px; +} diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 854f195c..c37bdb54 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -68,7 +68,8 @@ const AppLayout: React.FunctionComponent = ({ children }) => { label: 'Playground', children: [ { path: '/playground/chat', label: 'Chat' }, - { path: '/playground/endpoints', label: 'Custom Model Endpoints' } + { path: '/playground/endpoints', label: 'Custom Model Endpoints' }, + { path: '/playground/ragchat', label: 'Experimental RAG' } ] } ];