|
import React, { useCallback, useEffect, useState } from "react"; |
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
|
import { Label } from "@/components/ui/label"; |
|
import { Slider } from "@/components/ui/slider"; |
|
import { Button } from "@/components/ui/button"; |
|
import { |
|
Card, |
|
CardContent, |
|
CardDescription, |
|
CardHeader, |
|
CardTitle, |
|
} from "@/components/ui/card"; |
|
import { Input } from "@/components/ui/input"; |
|
import { Separator } from "@/components/ui/separator"; |
|
import { |
|
FileText, |
|
Link2, |
|
Settings, |
|
Volume2, |
|
Mic, |
|
Circle, |
|
CircleDot, |
|
Loader2, |
|
Text, |
|
} from "lucide-react"; |
|
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; |
|
import { |
|
setActiveVoice, |
|
setMaxTokens, |
|
setSilenceDuration, |
|
setTemperature, |
|
setThreshold, |
|
} from "@/redux/slices/settingsSlice"; |
|
import { |
|
setEmbeddingsText, |
|
clearEmbeddings, |
|
setisUploadingEmbeddings, |
|
setActiveChatPrompt, |
|
toggleSavingPrompt, |
|
} from "@/redux/slices/chatSlice"; |
|
import api from "@/api/apiClient"; |
|
import { toast } from "sonner"; |
|
import { Textarea } from "./ui/textarea"; |
|
import { cn } from "@/lib/utils"; |
|
|
|
const voices = [ |
|
{ id: 0, name: "Tripati (F)", voice_name: "af" }, |
|
{ id: 1, name: "Bella (F)", voice_name: "af_bella" }, |
|
{ id: 2, name: "Sarah (F)", voice_name: "af_sarah" }, |
|
{ id: 3, name: "Adam (M)", voice_name: "am_adam" }, |
|
{ id: 4, name: "Michael (M)", voice_name: "am_michael" }, |
|
{ id: 5, name: "Emma (F)", voice_name: "bf_emma" }, |
|
{ |
|
id: 6, |
|
name: "Isabella (F)", |
|
voice_name: "bf_isabella", |
|
}, |
|
{ id: 7, name: "George (M)", voice_name: "bm_george" }, |
|
{ id: 8, name: "Lewis (M)", voice_name: "bm_lewis" }, |
|
{ id: 9, name: "Nicole (F)", voice_name: "af_nicole" }, |
|
{ id: 10, name: "Sky (M)", voice_name: "af_sky" }, |
|
]; |
|
|
|
const ControlPanel = () => { |
|
const [linkUrl, setLinkUrl] = useState(""); |
|
const [text, settext] = useState(""); |
|
const [title, setTitle] = useState(""); |
|
const [summary, setSummary] = useState(""); |
|
const [categories, setCategories] = useState(""); |
|
const [selectedTab, setSelectedTab] = useState("voice"); |
|
const [contextDeleting, setContextDeleting] = useState(false); |
|
const [selectedFile, setSelectedFile] = useState<File | null>(null); |
|
const dispatch = useAppDispatch(); |
|
const { activeChat } = useAppSelector((state) => state.chats); |
|
const { |
|
activeChatContext, |
|
uploadingEmbeddings, |
|
activeChatPrompt, |
|
savingPrompt, |
|
} = useAppSelector((state) => state.chat); |
|
const [localPromptState, setLocalPromptState] = useState(""); |
|
|
|
useEffect(() => { |
|
setLocalPromptState(activeChatPrompt); |
|
}, [activeChatPrompt]); |
|
|
|
const { activeVoice, temperature, maxTokens, silenceDuration, threshold } = |
|
useAppSelector((state) => state.settings); |
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
if (e.target.files && e.target.files[0]) { |
|
setSelectedFile(e.target.files[0]); |
|
} |
|
}; |
|
|
|
const handleEmbeddingList = async ( |
|
name: string, |
|
title: string, |
|
summary: string, |
|
categories: string |
|
) => { |
|
let categoriesList = []; |
|
if (categories != null) { |
|
if (typeof categories === "string") { |
|
categoriesList = categories |
|
.split(",") |
|
.map((cat) => cat.trim()) |
|
.filter((cat) => cat.length > 0); |
|
} else if (Array.isArray(categories)) { |
|
categoriesList = categories.map((cat) => String(cat)); |
|
} else { |
|
categoriesList = [String(categories)]; |
|
} |
|
} |
|
|
|
dispatch( |
|
setEmbeddingsText({ |
|
name: name, |
|
title: title, |
|
summary: summary, |
|
categories: categoriesList, |
|
}) |
|
); |
|
}; |
|
|
|
const cleatMetaData = () => { |
|
setTitle(""); |
|
setSummary(""); |
|
setCategories(""); |
|
}; |
|
|
|
const handleSavePrompt = async () => { |
|
if (localPromptState === activeChatPrompt) return; |
|
setLocalPromptState(activeChatPrompt); |
|
dispatch(toggleSavingPrompt()); |
|
try { |
|
if (!activeChatPrompt) { |
|
toast.error("Please add prompt to save the prompt."); |
|
return; |
|
} |
|
const response = await api.post("/prompt", { |
|
sessionId: activeChat, |
|
prompt: localPromptState, |
|
}); |
|
if (!response.success) { |
|
toast.error("Failed to save prompt. Please try again"); |
|
return; |
|
} |
|
dispatch(setActiveChatPrompt(localPromptState)); |
|
toast.success("System Prompt saved successfully."); |
|
} catch (_) { |
|
toast.error("Failed to save prompt. Please try again"); |
|
} finally { |
|
dispatch(toggleSavingPrompt()); |
|
} |
|
}; |
|
|
|
const handleLinkAdd = useCallback(async () => { |
|
try { |
|
dispatch(setisUploadingEmbeddings(true)); |
|
if (!linkUrl.trim()) { |
|
toast.error("Please provide a url"); |
|
return; |
|
} |
|
if (!activeChat) { |
|
toast.error("Please select or create a chat."); |
|
return; |
|
} |
|
const response = await api.post("/rag/link", { |
|
link: linkUrl, |
|
sessionId: activeChat, |
|
title: title, |
|
summary: summary, |
|
categories: categories, |
|
}); |
|
if (!response.success) { |
|
toast.error("failed to process link. Please try again"); |
|
return; |
|
} |
|
await handleEmbeddingList(linkUrl, title, summary, categories); |
|
setLinkUrl(""); |
|
} catch (error) { |
|
console.error("Error in processing link : ", error); |
|
if ( |
|
error.response && |
|
error.response.data && |
|
error.response.data.message |
|
) { |
|
toast.error(error.response.data.message); |
|
} else { |
|
|
|
toast.error("Failed to process link. Please try again."); |
|
} |
|
setLinkUrl(""); |
|
} finally { |
|
dispatch(setisUploadingEmbeddings(false)); |
|
|
|
} |
|
}, [activeChat, linkUrl, dispatch, title, summary, categories]); |
|
|
|
const handleTextUpload = useCallback(async () => { |
|
try { |
|
dispatch(setisUploadingEmbeddings(true)); |
|
console.log("MEtaData : ", { title, summary, categories }); |
|
if (!text.trim()) { |
|
toast.error("Please provide text"); |
|
return; |
|
} |
|
if (!activeChat) { |
|
toast.error("Please select or create a chat."); |
|
return; |
|
} |
|
const name = `${text.slice(0, 10)}...`; |
|
const response = await api.post("/rag/text", { |
|
text: text, |
|
sessionId: activeChat, |
|
title: title, |
|
name: name, |
|
summary: summary, |
|
categories: categories, |
|
}); |
|
if (!response.success) { |
|
toast.error("failed to process text. Please try again"); |
|
return; |
|
} |
|
await handleEmbeddingList(name, title, summary, categories); |
|
} catch (error) { |
|
console.error("Error in processing text : ", error); |
|
if ( |
|
error.response && |
|
error.response.data && |
|
error.response.data.message |
|
) { |
|
toast.error(error.response.data.message); |
|
} else { |
|
toast.error("Failed to process text. Please try again."); |
|
} |
|
} finally { |
|
settext(""); |
|
dispatch(setisUploadingEmbeddings(false)); |
|
cleatMetaData(); |
|
} |
|
}, [activeChat, text, dispatch, title, summary, categories]); |
|
|
|
const handlePdfUpload = useCallback(async () => { |
|
dispatch(setisUploadingEmbeddings(true)); |
|
if (!activeChat) { |
|
toast.error("Please select or create a chat."); |
|
return; |
|
} |
|
if (!selectedFile) { |
|
toast.error("Please select a PDF file to upload."); |
|
return; |
|
} |
|
const form = new FormData(); |
|
form.append("pdfFile", selectedFile); |
|
form.append("name", selectedFile.name); |
|
form.append("title", title); |
|
form.append("summary", summary); |
|
form.append("categories", categories); |
|
form.append("sessionId", activeChat); |
|
|
|
const config = { |
|
headers: { |
|
"Content-Type": "multipart/form-data", |
|
}, |
|
}; |
|
|
|
try { |
|
const response = await api.post("/rag/pdf", form, config); |
|
if (!response.success) { |
|
toast.error("Failed to upload PDF. Please try again."); |
|
} |
|
await handleEmbeddingList(selectedFile.name, title, summary, categories); |
|
setSelectedFile(null); |
|
toast.success("PDF uploaded successfully!"); |
|
} catch (error) { |
|
console.error("Error uploading PDF:", error); |
|
toast.error("Failed to upload PDF. Please try again."); |
|
} finally { |
|
dispatch(setisUploadingEmbeddings(false)); |
|
cleatMetaData(); |
|
} |
|
}, [selectedFile, activeChat, dispatch, title, summary, categories]); |
|
|
|
const handleDelete = async () => { |
|
try { |
|
if (!activeChat) { |
|
toast.error("Please select a chat first"); |
|
return; |
|
} |
|
const response = await api.post("/rag/clear-context", { |
|
sessionId: activeChat, |
|
}); |
|
if (response.success) { |
|
toast.success("Context cleard successfully"); |
|
dispatch(clearEmbeddings()); |
|
return; |
|
} else { |
|
toast.error("Failed to clear embeddings. Please try again"); |
|
} |
|
} catch (error) { |
|
toast.error("Failed to clear embeddings. Please try again"); |
|
} finally { |
|
setContextDeleting(false); |
|
} |
|
}; |
|
|
|
const metadataForData = () => { |
|
return ( |
|
<div className="w-full space-y-2"> |
|
<Input |
|
type="url" |
|
placeholder="Title" |
|
className="flex-1 bg-black/60 border-white/20 text-white placeholder:text-gray-500 mt-2" |
|
value={title} |
|
onChange={(e) => setTitle(e.target.value)} |
|
/> |
|
|
|
<Textarea |
|
className="border border-white/20 bg-transparent text-white" |
|
value={summary} |
|
rows={5} |
|
placeholder="Provide a summary of the content that can help improve outcomes." |
|
onChange={(e) => setSummary(e.target.value)} |
|
/> |
|
<Textarea |
|
className="border border-white/20 bg-transparent text-white" |
|
value={categories} |
|
rows={5} |
|
placeholder={`Provide Categories (separated by commas) \neg: React New Features, Kokoro TTS Model, VocRT Documentation`} |
|
onChange={(e) => setCategories(e.target.value)} |
|
/> |
|
</div> |
|
); |
|
}; |
|
|
|
return ( |
|
<div className="w-full lg:w-80 h-full bg-black overflow-y-auto p-4 border-l border-white/10 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"> |
|
<div className="relative overflow-hidden text-sm"> |
|
<Tabs |
|
defaultValue="voice" |
|
value={selectedTab} |
|
onValueChange={setSelectedTab} |
|
className="w-full relative z-10" |
|
> |
|
<TabsList className="grid grid-cols-3 w-full bg-black border border-white/20 m-auto"> |
|
<TabsTrigger |
|
value="voice" |
|
className="data-[state=active]:bg-white/10 data-[state=active]:text-white" |
|
> |
|
<Mic className="h-4 w-4 mr-2" /> |
|
Voice |
|
</TabsTrigger> |
|
<TabsTrigger |
|
value="ai" |
|
className="data-[state=active]:bg-white/10 data-[state=active]:text-white" |
|
> |
|
AI Settings |
|
</TabsTrigger> |
|
<TabsTrigger |
|
value="embeddings" |
|
className="data-[state=active]:bg-white/10 data-[state=active]:text-white" |
|
> |
|
<Link2 className="h-4 w-4 mr-2" /> |
|
Context |
|
</TabsTrigger> |
|
</TabsList> |
|
|
|
<TabsContent value="voice" className="mt-4"> |
|
<Card className="bg-black/90 border border-white/20 backdrop-blur-lg"> |
|
<CardHeader> |
|
<CardTitle className="text-white flex items-center"> |
|
<Volume2 className="h-5 w-5 mr-2 text-gray-400" /> |
|
Voice Selection |
|
</CardTitle> |
|
<CardDescription className="text-gray-400"> |
|
Choose a voice for your AI assistant |
|
</CardDescription> |
|
</CardHeader> |
|
<CardContent> |
|
{voices.map((voice) => ( |
|
<div |
|
key={voice.id} |
|
onClick={() => { |
|
dispatch(setActiveVoice(voice?.voice_name)); |
|
}} |
|
className={`flex items-center space-x-2 rounded-md border border-white/20 p-2 hover:bg-white/5 transition cursor-pointer mb-2 ${ |
|
activeVoice === voice?.voice_name ? "bg-white/10" : "" |
|
}`} |
|
> |
|
<div className="icon-container"> |
|
{activeVoice === voice?.voice_name ? ( |
|
<CircleDot className="text-white" /> |
|
) : ( |
|
<Circle className="text-white" /> |
|
)} |
|
</div> |
|
<div className="flex-1 cursor-pointer text-white"> |
|
{voice.name} |
|
</div> |
|
{voice.id === 0 && ( |
|
<div className="text-xs bg-white/10 text-gray-300 px-2 py-1 rounded"> |
|
Default |
|
</div> |
|
)} |
|
</div> |
|
))} |
|
</CardContent> |
|
</Card> |
|
</TabsContent> |
|
|
|
<TabsContent |
|
value="ai" |
|
className="mt-4 space-y-2 overflow-x-hidden overflow-y-auto" |
|
> |
|
<> |
|
<Card className="bg-black/90 border border-white/20 backdrop-blur-lg"> |
|
<CardHeader> |
|
<CardTitle className="text-white flex items-center"> |
|
<Settings className="h-5 w-5 mr-2 text-gray-400" /> |
|
System Prompt |
|
</CardTitle> |
|
<CardDescription className="text-gray-400"> |
|
Set of instructions, guidelines, and contextual information |
|
</CardDescription> |
|
</CardHeader> |
|
<CardContent className="space-y-6"> |
|
<div className="space-y-2"> |
|
<Textarea |
|
className="border border-white bg-transparent text-white" |
|
id="text-upload" |
|
placeholder="You are a helpful assistant." |
|
value={localPromptState} |
|
rows={10} |
|
onChange={(e) => { |
|
setLocalPromptState(e.target.value); |
|
}} |
|
/> |
|
<Button |
|
className="w-full mt-2 bg-white/10 hover:bg-white/20 text-white border border-white/20" |
|
onClick={handleSavePrompt} |
|
disabled={ |
|
savingPrompt || activeChatPrompt === localPromptState |
|
} |
|
> |
|
{savingPrompt ? ( |
|
<Loader2 className="animate-spin" /> |
|
) : ( |
|
"Save" |
|
)} |
|
</Button> |
|
|
|
<CardHeader> |
|
<CardTitle className="text-white flex items-center"> |
|
<Settings className="h-5 w-5 mr-2 text-gray-400" /> |
|
AI Parameters |
|
</CardTitle> |
|
<CardDescription className="text-gray-400"> |
|
Adjust the AI response settings |
|
</CardDescription> |
|
</CardHeader> |
|
<CardContent className="space-y-6"> |
|
<div className="space-y-2"> |
|
<div className="flex justify-between"> |
|
<Label |
|
htmlFor="temperature" |
|
className="text-white flex items-center" |
|
> |
|
Silence Duration: {silenceDuration.toFixed(1)} |
|
</Label> |
|
</div> |
|
<Slider |
|
id="silenceDuration" |
|
min={0} |
|
max={2} |
|
step={0.1} |
|
value={[silenceDuration]} |
|
onValueChange={(value) => { |
|
dispatch(setSilenceDuration(value[0])); |
|
}} |
|
className="py-4" |
|
/> |
|
<p className="text-xs text-gray-400"> |
|
Lower values make VocRT to wait for short duration of |
|
silence. Higher values increase latency. |
|
</p> |
|
</div> |
|
<Separator className="bg-white/10" /> |
|
<div className="space-y-2"> |
|
<div className="flex justify-between"> |
|
<Label |
|
htmlFor="threshold" |
|
className="text-white flex items-center" |
|
> |
|
Threshold: {threshold} |
|
</Label> |
|
</div> |
|
<Slider |
|
id="threshold" |
|
min={5} |
|
max={95} |
|
step={1} |
|
value={[threshold]} |
|
onValueChange={(value) => { |
|
dispatch(setThreshold(value[0])); |
|
}} |
|
className="py-4 ease-in transition-all duration-300" |
|
/> |
|
<p className="text-xs text-gray-400"> |
|
Lower values make VocRT to listen to low amplitude |
|
voice. Higher values make VocRT to listen to your |
|
Voice. |
|
</p> |
|
</div> |
|
{/* <Separator className="bg-white/10" /> |
|
<div className="space-y-2"> |
|
<div className="flex justify-between"> |
|
<Label |
|
htmlFor="temperature" |
|
className="text-white flex items-center" |
|
> |
|
Temperature: {temperature.toFixed(1)} |
|
</Label> |
|
</div> |
|
<Slider |
|
id="temperature" |
|
min={0} |
|
max={1} |
|
step={0.1} |
|
value={[temperature]} |
|
onValueChange={(value) => { |
|
dispatch(setTemperature(value[0])); |
|
}} |
|
className="py-4" |
|
/> |
|
<p className="text-xs text-gray-400"> |
|
Lower values make responses more focused. Higher |
|
values make responses more creative. |
|
</p> |
|
</div> */} |
|
|
|
<Separator className="bg-white/10" /> |
|
|
|
{/* <div className="space-y-2"> |
|
<div className="flex justify-between"> |
|
<Label htmlFor="max-tokens" className="text-white"> |
|
Max Tokens: {maxTokens} |
|
</Label> |
|
</div> |
|
<Slider |
|
id="max-tokens" |
|
min={50} |
|
max={500} |
|
step={10} |
|
value={[maxTokens]} |
|
onValueChange={(value) => { |
|
dispatch(setMaxTokens(value[0])); |
|
}} |
|
className="py-4" |
|
/> |
|
<p className="text-xs text-gray-400"> |
|
Controls the maximum length of the AI's response. |
|
</p> |
|
</div> */} |
|
</CardContent> |
|
</div> |
|
</CardContent> |
|
</Card> |
|
</> |
|
</TabsContent> |
|
|
|
<TabsContent value="embeddings" className="mt-4"> |
|
<Card className="bg-black/90 border border-white/20 backdrop-blur-lg"> |
|
<CardHeader> |
|
<CardTitle className="text-white flex items-center"> |
|
<Link2 className="h-5 w-5 mr-2 text-gray-400" /> |
|
Context & Embeddings |
|
</CardTitle> |
|
<CardDescription className="text-gray-400"> |
|
Add additional context from various sources |
|
</CardDescription> |
|
</CardHeader> |
|
<CardContent className="space-y-4"> |
|
<Tabs defaultValue="text" className="w-full"> |
|
<TabsList className="grid grid-cols-3 space-x-2 w-full bg-black border border-white/20"> |
|
<TabsTrigger |
|
value="text" |
|
className="text-xs data-[state=active]:bg-white/10 data-[state=active]:text-white" |
|
> |
|
<Text className="h-3 w-3 mr-1" /> Text |
|
</TabsTrigger> |
|
<TabsTrigger |
|
value="pdf" |
|
className="text-xs data-[state=active]:bg-white/10 data-[state=active]:text-white" |
|
> |
|
<FileText className="h-3 w-3 mr-1" /> PDF |
|
</TabsTrigger> |
|
<TabsTrigger |
|
value="link" |
|
className="text-xs data-[state=active]:bg-white/10 data-[state=active]:text-white" |
|
> |
|
<Link2 className="h-3 w-3 mr-1" /> Link |
|
</TabsTrigger> |
|
</TabsList> |
|
|
|
<TabsContent value="text" className="mt-4 w-full"> |
|
<Textarea |
|
className="border border-white bg-transparent text-white" |
|
id="text-upload" |
|
placeholder="Add Context..." |
|
value={text} |
|
rows={8} |
|
onChange={(e) => { |
|
settext(e.target.value); |
|
}} |
|
/> |
|
{metadataForData()} |
|
<Button |
|
className="w-full mt-2 bg-white/10 hover:bg-white/20 text-white border border-white/20" |
|
onClick={handleTextUpload} |
|
disabled={uploadingEmbeddings} |
|
> |
|
{uploadingEmbeddings ? ( |
|
<Loader2 className="animate-spin" /> |
|
) : ( |
|
"Process Text" |
|
)} |
|
</Button> |
|
</TabsContent> |
|
|
|
<TabsContent value="pdf" className="mt-4"> |
|
<div |
|
className={cn( |
|
"border-2 border-dashed border-white/20 rounded-md p-6 text-center bg-white/5 hover:bg-white/10 transition cursor-pointer", |
|
uploadingEmbeddings |
|
? "hover:cursor-not-allowed" |
|
: "hover:cursor-pointer" |
|
)} |
|
> |
|
<Input |
|
type="file" |
|
className={cn( |
|
"hidden", |
|
uploadingEmbeddings |
|
? "hover:cursor-not-allowed" |
|
: "hover:cursor-pointer" |
|
)} |
|
id="pdf-upload" |
|
accept=".pdf,.csv,.ppt,.pptx,.doc,.docx,.xls,.xlsx,.txt" |
|
onChange={handleFileChange} |
|
disabled={uploadingEmbeddings} |
|
/> |
|
<Label |
|
htmlFor="pdf-upload" |
|
className={cn( |
|
"cursor-pointer", |
|
uploadingEmbeddings |
|
? "hover:cursor-not-allowed" |
|
: "hover:cursor-pointer" |
|
)} |
|
> |
|
<FileText className="h-10 w-10 mx-auto mb-2 text-gray-400" /> |
|
<p |
|
className={cn( |
|
"text-sm text-white mb-1", |
|
uploadingEmbeddings |
|
? "hover:cursor-not-allowed" |
|
: "hover:cursor-pointer" |
|
)} |
|
> |
|
{selectedFile ? selectedFile.name : "Upload Document"} |
|
</p> |
|
<p className="text-xs text-gray-400"> |
|
Drag & drop or click to browse |
|
</p> |
|
<p className="text-xs text-gray-400 mt-3"> |
|
Accept pdf, csv, ppt, pptx, doc, docx, xls, xlsx, txt |
|
</p> |
|
</Label> |
|
</div> |
|
{selectedFile && metadataForData()} |
|
{(selectedFile || uploadingEmbeddings) && ( |
|
<Button |
|
className="w-full mt-2 bg-white/10 hover:bg-white/20 text-white border border-white/20" |
|
onClick={handlePdfUpload} |
|
disabled={uploadingEmbeddings} |
|
> |
|
{uploadingEmbeddings ? ( |
|
<Loader2 className="animate-spin" /> |
|
) : ( |
|
"Process Document" |
|
)} |
|
</Button> |
|
)} |
|
</TabsContent> |
|
|
|
<TabsContent value="link" className="mt-4 space-y-3 w-full"> |
|
<div className=""> |
|
<Input |
|
type="url" |
|
placeholder="https://example.com" |
|
className="flex-1 bg-black/60 border-white/20 text-white placeholder:text-gray-500" |
|
value={linkUrl} |
|
onChange={(e) => setLinkUrl(e.target.value)} |
|
/> |
|
{metadataForData()} |
|
<Button |
|
className="bg-white/10 hover:bg-white/20 text-white border border-white/20 w-full mt-3" |
|
onClick={handleLinkAdd} |
|
> |
|
{uploadingEmbeddings ? ( |
|
<Loader2 className="animate-spin" /> |
|
) : ( |
|
<> |
|
<Link2 className="h-4 w-4 mr-1" /> Add |
|
</> |
|
)} |
|
</Button> |
|
</div> |
|
</TabsContent> |
|
</Tabs> |
|
|
|
{activeChatContext && activeChatContext.length > 0 && ( |
|
<> |
|
<h4 className="text-sm font-medium text-white mb-2"> |
|
Added Context: |
|
</h4> |
|
<Separator className="bg-white/10 my-4" /> |
|
|
|
{activeChatContext.map((item, index) => ( |
|
<> |
|
<div> |
|
<div className="bg-white/5 p-3 rounded-md text-sm text-gray-300 max-h-40 overflow-y-auto border border-white/10"> |
|
{item?.title} - {item?.name} |
|
</div> |
|
</div> |
|
</> |
|
))} |
|
|
|
<Button |
|
variant="outline" |
|
className="w-full border-white/20 text-gray-900 hover:bg-white/10 hover:text-white" |
|
onClick={() => { |
|
setContextDeleting(true); |
|
handleDelete(); |
|
}} |
|
disabled={contextDeleting} |
|
> |
|
{contextDeleting ? "Deleting..." : "Clear All Context"} |
|
</Button> |
|
</> |
|
)} |
|
</CardContent> |
|
</Card> |
|
</TabsContent> |
|
</Tabs> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default ControlPanel; |
|
|