| import { useState, useEffect, useRef } from "react"; |
| import { useNavigate } from "react-router-dom"; |
| import { Button } from "@/components/ui/button"; |
| import { Card } from "@/components/ui/card"; |
| import { Input } from "@/components/ui/input"; |
| import { Label } from "@/components/ui/label"; |
| import { Textarea } from "@/components/ui/textarea"; |
| import { |
| Select, |
| SelectContent, |
| SelectItem, |
| SelectTrigger, |
| SelectValue, |
| } from "@/components/ui/select"; |
| import { Slider } from "@/components/ui/slider"; |
| import { Badge } from "@/components/ui/badge"; |
| import { ASSISTANT_CONFIGS } from "@/config/assistants"; |
| import { useChat } from "@/hooks/useChat"; |
| import { DocumentsTab } from "@/components/playground/DocumentsTab"; |
|
|
| interface DeviceConfig { |
| assistantId: string; |
| model: string; |
| temperature: number; |
| maxTokens: number; |
| role: string; |
| goal: string; |
| description: string; |
| document: string; |
| } |
|
|
| interface SelectedAssistant { |
| id: string; |
| name: string; |
| type: "user" | "template" | "new"; |
| originalTemplate?: string; |
| } |
|
|
| export function MyDevice() { |
| const navigate = useNavigate(); |
| const [currentStep, setCurrentStep] = useState(1); |
| const [deviceConfig, setDeviceConfig] = useState<DeviceConfig>({ |
| assistantId: "", |
| model: "Qwen/Qwen3-30B-A3B", |
| temperature: 0.7, |
| maxTokens: 1024, |
| role: "", |
| goal: "", |
| description: "", |
| document: "", |
| }); |
|
|
| |
| const [ragEnabled, setRagEnabled] = useState(false); |
| const [retrievalCount, setRetrievalCount] = useState(3); |
| const [currentAssistant, setCurrentAssistant] = |
| useState<SelectedAssistant | null>(null); |
|
|
| |
| const messagesEndRef = useRef<HTMLDivElement>(null); |
|
|
| |
| const scrollToBottom = () => { |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); |
| }; |
|
|
| |
| const handleAssistantSelect = (assistantId: string) => { |
| if (assistantId === "create-new") { |
| |
| setDeviceConfig((prev) => ({ |
| ...prev, |
| assistantId: "create-new", |
| model: "Qwen/Qwen3-30B-A3B", |
| temperature: 0.7, |
| maxTokens: 1024, |
| role: "", |
| goal: "", |
| description: "", |
| })); |
|
|
| |
| setSelectedModel("Qwen/Qwen3-30B-A3B"); |
| setSystemPrompt(""); |
| setTemperature(0.7); |
| setMaxTokens(1024); |
|
|
| |
| setCurrentAssistant({ |
| id: "create-new", |
| name: "New Custom Assistant", |
| type: "new", |
| }); |
| } else { |
| const config = assistantConfigs.find((c) => c.id === assistantId); |
| if (config) { |
| setDeviceConfig((prev) => ({ |
| ...prev, |
| assistantId, |
| model: config.model, |
| temperature: config.temperature, |
| maxTokens: config.maxTokens, |
| role: config.description, |
| goal: config.category, |
| description: config.systemPrompt, |
| })); |
|
|
| |
| setSelectedModel(config.model); |
| setSystemPrompt(config.systemPrompt); |
| setTemperature(config.temperature); |
| setMaxTokens(config.maxTokens); |
|
|
| |
| setCurrentAssistant({ |
| id: assistantId, |
| name: config.name, |
| type: "template", |
| }); |
| } |
| } |
| }; |
|
|
| |
| useEffect(() => { |
| const selectedUseCase = localStorage.getItem("selectedUseCase"); |
| if (selectedUseCase) { |
| handleAssistantSelect(selectedUseCase); |
| localStorage.removeItem("selectedUseCase"); |
| } |
| }, []); |
|
|
| |
| const { |
| currentSession, |
| createNewSession, |
| sendMessage, |
| isLoading, |
| setSystemPrompt, |
| setTemperature, |
| setMaxTokens, |
| setSelectedModel, |
| input, |
| setInput, |
| } = useChat(); |
|
|
| const assistantConfigs = ASSISTANT_CONFIGS; |
| const messages = currentSession?.messages || []; |
|
|
| |
| useEffect(() => { |
| scrollToBottom(); |
| }, [messages]); |
|
|
| |
| useEffect(() => { |
| setSelectedModel(deviceConfig.model); |
| setSystemPrompt(deviceConfig.description); |
| setTemperature(deviceConfig.temperature); |
| setMaxTokens(deviceConfig.maxTokens); |
| }, [ |
| deviceConfig, |
| setSelectedModel, |
| setSystemPrompt, |
| setTemperature, |
| setMaxTokens, |
| ]); |
|
|
| const renderStepIndicator = () => ( |
| <div className="flex items-center justify-between mb-8 max-w-4xl mx-auto"> |
| <div className="flex items-center"> |
| <div |
| className={`flex items-center ${ |
| currentStep >= 1 ? "text-purple-600" : "text-gray-400" |
| }`} |
| > |
| <div |
| className={`w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-medium ${ |
| currentStep >= 1 |
| ? "bg-purple-600 text-white border-purple-600" |
| : "border-gray-300" |
| }`} |
| > |
| 1 |
| </div> |
| <span className="ml-2 font-medium">Configuration</span> |
| </div> |
| <div |
| className={`w-32 h-0.5 mx-4 ${ |
| currentStep >= 2 ? "bg-purple-600" : "bg-gray-300" |
| }`} |
| /> |
| </div> |
| |
| <div className="flex items-center"> |
| <div |
| className={`flex items-center ${ |
| currentStep >= 2 ? "text-purple-600" : "text-gray-400" |
| }`} |
| > |
| <div |
| className={`w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-medium ${ |
| currentStep >= 2 |
| ? "bg-purple-600 text-white border-purple-600" |
| : "border-gray-300" |
| }`} |
| > |
| 2 |
| </div> |
| <span className="ml-2 font-medium">Test performance</span> |
| </div> |
| <div |
| className={`w-32 h-0.5 mx-4 ${ |
| currentStep >= 3 ? "bg-purple-600" : "bg-gray-300" |
| }`} |
| /> |
| </div> |
| |
| <div className="flex items-center"> |
| <div |
| className={`flex items-center ${ |
| currentStep >= 3 ? "text-purple-600" : "text-gray-400" |
| }`} |
| > |
| <div |
| className={`w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-medium ${ |
| currentStep >= 3 |
| ? "bg-purple-600 text-white border-purple-600" |
| : "border-gray-300" |
| }`} |
| > |
| 3 |
| </div> |
| <span className="ml-2 font-medium">Deploy to device</span> |
| </div> |
| <Button |
| className="ml-8 bg-purple-600 hover:bg-purple-700 text-white px-8" |
| onClick={() => setCurrentStep(3)} |
| disabled={currentStep < 2} |
| > |
| Connect |
| </Button> |
| </div> |
| </div> |
| ); |
|
|
| const renderStep1 = () => ( |
| <div className="max-w-2xl mx-auto"> |
| <Card className="p-8"> |
| <div className="space-y-6"> |
| {/* Load Assistant */} |
| <div> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Load Assistant |
| </Label> |
| <Select |
| onValueChange={handleAssistantSelect} |
| value={deviceConfig.assistantId} |
| > |
| <SelectTrigger> |
| <SelectValue placeholder="Select an assistant or create new" /> |
| </SelectTrigger> |
| <SelectContent> |
| <SelectItem value="create-new"> |
| <div className="flex items-center gap-2"> |
| <span className="text-lg">✨</span> |
| <span>Create New Assistant</span> |
| </div> |
| </SelectItem> |
| <div className="border-b my-1"></div> |
| {assistantConfigs.map((config) => ( |
| <SelectItem key={config.id} value={config.id}> |
| <div className="flex items-center gap-2"> |
| <span>{config.icon}</span> |
| <span>{config.name.replace(/🏔️|🏥|🧸/g, "").trim()}</span> |
| </div> |
| </SelectItem> |
| ))} |
| </SelectContent> |
| </Select> |
| </div> |
| |
| {/* Parameters */} |
| <div> |
| <h3 className="text-lg font-semibold mb-4">Parameters</h3> |
| |
| {/* Model Selection */} |
| <div className="mb-4"> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Select a model |
| </Label> |
| <Select |
| value={deviceConfig.model} |
| onValueChange={(value) => |
| setDeviceConfig((prev) => ({ ...prev, model: value })) |
| } |
| > |
| <SelectTrigger> |
| <SelectValue /> |
| </SelectTrigger> |
| <SelectContent> |
| <SelectItem value="Qwen/Qwen3-30B-A3B"> |
| Qwen3-30B-A3B |
| </SelectItem> |
| <SelectItem value="Qwen/Qwen2.5-32B-Instruct"> |
| Qwen2.5-32B-Instruct |
| </SelectItem> |
| <SelectItem value="deepseek-ai/DeepSeek-V2.5"> |
| DeepSeek-V2.5 |
| </SelectItem> |
| </SelectContent> |
| </Select> |
| </div> |
| |
| {/* Temperature */} |
| <div className="mb-4"> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Temperature: {deviceConfig.temperature} |
| </Label> |
| <Slider |
| value={[deviceConfig.temperature]} |
| onValueChange={(value) => |
| setDeviceConfig((prev) => ({ |
| ...prev, |
| temperature: value[0], |
| })) |
| } |
| max={1} |
| min={0} |
| step={0.1} |
| className="w-full" |
| /> |
| </div> |
| |
| {/* Max Tokens */} |
| <div className="mb-6"> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Max Tokens |
| </Label> |
| <Input |
| type="number" |
| value={deviceConfig.maxTokens} |
| onChange={(e) => |
| setDeviceConfig((prev) => ({ |
| ...prev, |
| maxTokens: parseInt(e.target.value) || 1024, |
| })) |
| } |
| min={1} |
| max={4096} |
| /> |
| </div> |
| </div> |
| |
| {/* Description */} |
| <div> |
| <h3 className="text-lg font-semibold mb-4">Description</h3> |
| |
| <div className="mb-4"> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Which role do you want your assistant to be? (Senior data |
| researcher, journalist, lawyer, etc.) |
| </Label> |
| <Input |
| value={deviceConfig.role} |
| onChange={(e) => |
| setDeviceConfig((prev) => ({ ...prev, role: e.target.value })) |
| } |
| placeholder="e.g., Senior data researcher, journalist, lawyer" |
| /> |
| </div> |
| |
| <div className="mb-4"> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Which goal would you expect your assistant to achieve |
| </Label> |
| <Input |
| value={deviceConfig.goal} |
| onChange={(e) => |
| setDeviceConfig((prev) => ({ ...prev, goal: e.target.value })) |
| } |
| placeholder="Describe the main goal or objective" |
| /> |
| </div> |
| |
| <div className="mb-6"> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Description (Tasks, outputs) |
| </Label> |
| <Textarea |
| value={deviceConfig.description} |
| onChange={(e) => |
| setDeviceConfig((prev) => ({ |
| ...prev, |
| description: e.target.value, |
| })) |
| } |
| placeholder="Describe the tasks and expected outputs..." |
| rows={4} |
| /> |
| </div> |
| </div> |
| |
| {/* Document - RAG Integration */} |
| <div> |
| <Label className="text-sm font-medium text-gray-700 mb-2 block"> |
| Documents & Knowledge Base |
| </Label> |
| <DocumentsTab |
| isLoading={isLoading} |
| ragEnabled={ragEnabled} |
| setRagEnabled={setRagEnabled} |
| retrievalCount={retrievalCount} |
| setRetrievalCount={setRetrievalCount} |
| currentAssistant={currentAssistant} |
| /> |
| </div> |
| |
| <Button |
| onClick={() => setCurrentStep(2)} |
| className="w-full bg-purple-600 hover:bg-purple-700 text-white" |
| disabled={ |
| !deviceConfig.assistantId || |
| (deviceConfig.assistantId === "create-new" && |
| !deviceConfig.description.trim()) |
| } |
| > |
| Continue to Test Performance |
| </Button> |
| </div> |
| </Card> |
| </div> |
| ); |
|
|
| const renderStep2 = () => ( |
| <div className="max-w-6xl mx-auto"> |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-[500px]"> |
| {/* Chat Sessions Sidebar */} |
| <div className="lg:col-span-1"> |
| <Card className="h-full p-4"> |
| <h3 className="font-semibold mb-4">Chat sessions</h3> |
| <div className="space-y-2"> |
| <Button |
| variant="ghost" |
| className="w-full justify-start text-left" |
| onClick={createNewSession} |
| > |
| New Chat |
| </Button> |
| {messages.length > 0 && ( |
| <div className="p-2 bg-gray-50 rounded text-sm"> |
| Current Session ({messages.length} messages) |
| </div> |
| )} |
| </div> |
| </Card> |
| </div> |
| |
| {/* Chat Interface */} |
| <div className="lg:col-span-3"> |
| <Card className="h-full flex flex-col"> |
| <div className="p-4 border-b"> |
| <div className="flex items-center justify-between"> |
| <h3 className="font-semibold">Test Performance</h3> |
| <Badge variant="secondary"> |
| {deviceConfig.assistantId === "create-new" |
| ? "Custom Assistant" |
| : deviceConfig.assistantId |
| ? assistantConfigs |
| .find((c) => c.id === deviceConfig.assistantId) |
| ?.name.replace(/🏔️|🏥|🧸/g, "") |
| .trim() |
| : "No Assistant"} |
| </Badge> |
| </div> |
| </div> |
| |
| <div className="flex-1 overflow-y-auto p-4 space-y-4 max-h-96 min-h-0"> |
| {messages.length === 0 ? ( |
| <div className="flex items-center justify-center h-full text-gray-500"> |
| <div className="text-center"> |
| <p className="mb-2"> |
| Start testing your assistant configuration |
| </p> |
| <p className="text-sm"> |
| Send a message to see how your assistant responds |
| </p> |
| </div> |
| </div> |
| ) : ( |
| messages.map((message, index) => ( |
| <div |
| key={index} |
| className={`flex ${ |
| message.role === "user" ? "justify-end" : "justify-start" |
| }`} |
| > |
| <div |
| className={`max-w-[80%] p-3 rounded-lg ${ |
| message.role === "user" |
| ? "bg-purple-600 text-white" |
| : "bg-gray-100 text-gray-900" |
| }`} |
| > |
| <p className="whitespace-pre-wrap">{message.content}</p> |
| </div> |
| </div> |
| )) |
| )} |
| {isLoading && ( |
| <div className="flex justify-start"> |
| <div className="bg-gray-100 p-3 rounded-lg"> |
| <div className="flex items-center space-x-2"> |
| <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div> |
| <div |
| className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" |
| style={{ animationDelay: "0.1s" }} |
| ></div> |
| <div |
| className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" |
| style={{ animationDelay: "0.2s" }} |
| ></div> |
| </div> |
| </div> |
| </div> |
| )} |
| <div ref={messagesEndRef} /> |
| </div> |
| |
| <div className="p-4 border-t"> |
| <div className="flex space-x-2"> |
| <Input |
| placeholder="Type your message..." |
| value={input} |
| onChange={(e) => setInput(e.target.value)} |
| onKeyPress={(e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| if (input.trim()) { |
| sendMessage(undefined, { |
| useRag: ragEnabled, |
| retrievalCount, |
| }); |
| } |
| } |
| }} |
| /> |
| <Button |
| onClick={() => { |
| if (input.trim()) { |
| sendMessage(undefined, { |
| useRag: ragEnabled, |
| retrievalCount, |
| }); |
| } |
| }} |
| disabled={isLoading} |
| className="bg-purple-600 hover:bg-purple-700" |
| > |
| Send |
| </Button> |
| </div> |
| </div> |
| </Card> |
| </div> |
| </div> |
| |
| <div className="mt-4 flex justify-between max-w-6xl mx-auto"> |
| <Button variant="outline" onClick={() => setCurrentStep(1)}> |
| Back to Configuration |
| </Button> |
| <Button |
| onClick={() => setCurrentStep(3)} |
| className="bg-purple-600 hover:bg-purple-700 text-white" |
| > |
| Continue to Deploy Device |
| </Button> |
| </div> |
| </div> |
| ); |
|
|
| const renderStep3 = () => ( |
| <div className="max-w-2xl mx-auto"> |
| <Card className="p-8 text-center"> |
| <div className="mb-6"> |
| <div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4"> |
| <svg |
| className="w-8 h-8 text-purple-600" |
| fill="none" |
| stroke="currentColor" |
| viewBox="0 0 24 24" |
| > |
| <path |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth={2} |
| d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.212 0" |
| /> |
| </svg> |
| </div> |
| <h2 className="text-2xl font-bold text-gray-900 mb-2"> |
| Deploy to Device |
| </h2> |
| <p className="text-gray-600"> |
| Your assistant configuration is ready to be deployed to your edge |
| device. |
| </p> |
| </div> |
| |
| <div className="space-y-4 text-left mb-8"> |
| <div className="bg-gray-50 p-4 rounded-lg"> |
| <h3 className="font-semibold mb-2">Configuration Summary</h3> |
| <div className="space-y-1 text-sm"> |
| <p> |
| <span className="font-medium">Assistant:</span>{" "} |
| {deviceConfig.assistantId === "create-new" |
| ? "Custom Assistant" |
| : assistantConfigs |
| .find((c) => c.id === deviceConfig.assistantId) |
| ?.name.replace(/🏔️|🏥|🧸/g, "") |
| .trim() || "None"} |
| </p> |
| <p> |
| <span className="font-medium">Model:</span> {deviceConfig.model} |
| </p> |
| <p> |
| <span className="font-medium">Temperature:</span>{" "} |
| {deviceConfig.temperature} |
| </p> |
| <p> |
| <span className="font-medium">Max Tokens:</span>{" "} |
| {deviceConfig.maxTokens} |
| </p> |
| {deviceConfig.role && ( |
| <p> |
| <span className="font-medium">Role:</span> {deviceConfig.role} |
| </p> |
| )} |
| <p> |
| <span className="font-medium">RAG Enabled:</span>{" "} |
| {ragEnabled ? "Yes" : "No"} |
| </p> |
| {ragEnabled && ( |
| <p> |
| <span className="font-medium">Retrieval Count:</span>{" "} |
| {retrievalCount} |
| </p> |
| )} |
| </div> |
| </div> |
| |
| <div className="bg-blue-50 p-4 rounded-lg border border-blue-200"> |
| <div className="flex items-center"> |
| <svg |
| className="w-5 h-5 text-blue-600 mr-2" |
| fill="none" |
| stroke="currentColor" |
| viewBox="0 0 24 24" |
| > |
| <path |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth={2} |
| d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" |
| /> |
| </svg> |
| <p className="text-blue-800 text-sm"> |
| <span className="font-medium">Ready to deploy:</span> Your |
| assistant configuration has been optimized for edge deployment. |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| <div className="space-y-3"> |
| <Button className="w-full bg-purple-600 hover:bg-purple-700 text-white py-3"> |
| Deploy to Device |
| </Button> |
| <Button |
| variant="outline" |
| className="w-full" |
| onClick={() => setCurrentStep(2)} |
| > |
| Back to Testing |
| </Button> |
| </div> |
| </Card> |
| </div> |
| ); |
|
|
| return ( |
| <div className="min-h-screen bg-gray-50"> |
| {/* Header */} |
| <header className="bg-white border-b border-gray-200"> |
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div className="flex items-center justify-between h-20"> |
| <div className="flex items-center space-x-2"> |
| <img |
| src="/assets/logo.png" |
| alt="EdgeLLM Logo" |
| className="h-16 w-16" |
| onError={(e) => { |
| console.error("Logo failed to load"); |
| e.currentTarget.style.display = "none"; |
| }} |
| /> |
| </div> |
| <nav className="flex space-x-8"> |
| <button |
| onClick={() => navigate("/")} |
| className="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium" |
| > |
| Home |
| </button> |
| <button |
| onClick={() => navigate("/technology")} |
| className="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium" |
| > |
| Technology |
| </button> |
| <button |
| onClick={() => navigate("/usecases")} |
| className="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium" |
| > |
| Use Case |
| </button> |
| <button className="bg-purple-600 text-white px-4 py-2 text-sm font-medium rounded"> |
| My Device |
| </button> |
| </nav> |
| </div> |
| </div> |
| </header> |
| |
| {/* Main Content */} |
| <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> |
| {renderStepIndicator()} |
| |
| {currentStep === 1 && renderStep1()} |
| {currentStep === 2 && renderStep2()} |
| {currentStep === 3 && renderStep3()} |
| </main> |
| </div> |
| ); |
| } |
|
|