VocRT / frontend /src /components /ControlPanel.tsx
Anurag
added threshold option and fixed some bugs
f1a245b
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 {
// Display a generic error message
toast.error("Failed to process link. Please try again.");
}
setLinkUrl("");
} finally {
dispatch(setisUploadingEmbeddings(false));
// cleatMetaData();
}
}, [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;