jurmy24 commited on
Commit
a7b9ee2
·
1 Parent(s): 912e2d8

refactor: get rid of unused stuff from old project

Browse files
viewer/src/api/urdfApi.ts DELETED
@@ -1,103 +0,0 @@
1
-
2
- import { supabase } from "@/integrations/supabase/client";
3
- import { Database } from "@/integrations/supabase/types";
4
- import { ContentItem, Category } from "@/lib/types";
5
-
6
- // Type for Urdf table row
7
- type UrdfRow = Database["public"]["Tables"]["urdf"]["Row"];
8
-
9
- // Convert a Supabase URDF row to our ContentItem format
10
- export const mapUrdfToContentItem = (urdf: UrdfRow): ContentItem => {
11
- // Convert tags to categories
12
- const categories = urdf.tags || [];
13
-
14
- return {
15
- id: urdf.id,
16
- title: urdf.name,
17
- imageUrl: urdf.image_uri || "/placeholder.svg", // This now contains the storage path or full URL
18
- description: urdf.summary || undefined,
19
- categories,
20
- urdfPath: urdf.urdf_uri || "",
21
- };
22
- };
23
-
24
- // Get all URDFs
25
- export const getUrdfs = async (): Promise<ContentItem[]> => {
26
- console.log("Fetching URDFs");
27
- const { data, error } = await supabase.from("urdf").select("*").order("name");
28
- console.log("URDFs fetched", data);
29
-
30
- if (error) {
31
- console.error("Error fetching URDFs:", error);
32
- throw error;
33
- }
34
-
35
- return (data || []).map(mapUrdfToContentItem);
36
- };
37
-
38
- // Get unique categories from all URDFs
39
- export const getCategories = async (): Promise<Category[]> => {
40
- const { data, error } = await supabase.from("urdf").select("tags");
41
-
42
- if (error) {
43
- console.error("Error fetching categories:", error);
44
- throw error;
45
- }
46
-
47
- // Extract all unique tags across all URDFs
48
- const allTags = new Set<string>();
49
- data.forEach((urdf) => {
50
- if (urdf.tags && Array.isArray(urdf.tags)) {
51
- urdf.tags.forEach((tag) => allTags.add(tag));
52
- }
53
- });
54
-
55
- // Convert to Category objects
56
- return Array.from(allTags).map((tag) => ({
57
- id: tag,
58
- name: tag.charAt(0).toUpperCase() + tag.slice(1), // Capitalize first letter
59
- }));
60
- };
61
-
62
- // Get URDF by ID
63
- export const getUrdfById = async (id: string): Promise<ContentItem | null> => {
64
- const { data, error } = await supabase
65
- .from("urdf")
66
- .select("*")
67
- .eq("id", id)
68
- .single();
69
-
70
- if (error) {
71
- if (error.code === "PGRST116") {
72
- // Row not found
73
- return null;
74
- }
75
- console.error("Error fetching URDF by ID:", error);
76
- throw error;
77
- }
78
-
79
- return mapUrdfToContentItem(data);
80
- };
81
-
82
- // Get public URL for image
83
- export const getImageUrl = async (path: string): Promise<string> => {
84
- if (!path || path.startsWith('http') || path.startsWith('data:')) {
85
- return path || '/placeholder.svg';
86
- }
87
-
88
- try {
89
- // Get public URL for the image
90
- const { data, error } = await supabase.storage
91
- .from('urdf-images')
92
- .createSignedUrl(path, 3600); // 1 hour expiry
93
-
94
- if (data && !error) {
95
- return data.signedUrl;
96
- }
97
- } catch (error) {
98
- console.error(`Error fetching image at ${path}:`, error);
99
- }
100
-
101
- return '/placeholder.svg';
102
- };
103
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/components/AnimationDialog.tsx DELETED
@@ -1,139 +0,0 @@
1
- import React, { useState } from "react";
2
- import {
3
- Dialog,
4
- DialogContent,
5
- DialogHeader,
6
- DialogTitle,
7
- DialogFooter,
8
- } from "./ui/dialog";
9
- import { Button } from "./ui/button";
10
- import { Textarea } from "./ui/textarea";
11
- import { Loader } from "lucide-react";
12
- import { toast } from "sonner";
13
- import { useCreateAnimation } from "@/hooks/useCreateAnimation";
14
- import { AnimationRequest, RobotAnimationConfig } from "@/lib/types";
15
- import { useUrdf } from "@/hooks/useUrdf";
16
-
17
- interface AnimationDialogProps {
18
- open: boolean;
19
- onOpenChange: (open: boolean) => void;
20
- robotName: string;
21
- }
22
-
23
- const AnimationDialog: React.FC<AnimationDialogProps> = ({
24
- open,
25
- onOpenChange,
26
- robotName,
27
- }) => {
28
- const [animationPrompt, setAnimationPrompt] = useState("");
29
- const [isGeneratingAnimation, setIsGeneratingAnimation] = useState(false);
30
- const { createAnimation, clearError } = useCreateAnimation();
31
- const { urdfContent, currentRobotData, setCurrentAnimationConfig } =
32
- useUrdf();
33
-
34
- // Handle dialog close - clear error states
35
- const handleOpenChange = (open: boolean) => {
36
- if (!open) {
37
- clearError();
38
- }
39
- onOpenChange(open);
40
- };
41
-
42
- const handleAnimationRequest = async () => {
43
- if (!animationPrompt.trim()) {
44
- toast.error("Please enter an animation description");
45
- return;
46
- }
47
-
48
- if (!urdfContent) {
49
- toast.error("No URDF content available", {
50
- description: "Please upload a URDF model first.",
51
- });
52
- return;
53
- }
54
-
55
- setIsGeneratingAnimation(true);
56
-
57
- try {
58
- // Create the animation request
59
- const request: AnimationRequest = {
60
- robotName: currentRobotData?.name || robotName,
61
- urdfContent,
62
- description: animationPrompt,
63
- };
64
-
65
- // Call the createAnimation function
66
- const animationResult = await createAnimation(request);
67
-
68
- setIsGeneratingAnimation(false);
69
- onOpenChange(false);
70
-
71
- // Reset the prompt for next time
72
- setAnimationPrompt("");
73
-
74
- // Store animation in the context instead of using the onAnimationApplied callback
75
- if (animationResult) {
76
- setCurrentAnimationConfig(animationResult);
77
-
78
- // Show success message
79
- toast.success("Animation applied successfully", {
80
- description: `Applied: "${animationPrompt}"`,
81
- duration: 2000,
82
- });
83
- }
84
- } catch (error) {
85
- setIsGeneratingAnimation(false);
86
-
87
- // The error toast is already handled in the hook
88
- console.error("Animation generation failed:", error);
89
- }
90
- };
91
-
92
- return (
93
- <Dialog open={open} onOpenChange={handleOpenChange}>
94
- <DialogContent className="sm:max-w-[425px] font-mono">
95
- <DialogHeader>
96
- <DialogTitle className="text-center">
97
- Animation Request for {currentRobotData?.name || robotName}
98
- </DialogTitle>
99
- </DialogHeader>
100
-
101
- <div className="py-4">
102
- <Textarea
103
- placeholder="Describe the animation you want to see (e.g., 'Make the robot wave its arm' or 'Make one of the wheels spin in circles')"
104
- className="min-h-[100px] font-mono"
105
- value={animationPrompt}
106
- onChange={(e) => setAnimationPrompt(e.target.value)}
107
- disabled={isGeneratingAnimation}
108
- />
109
- </div>
110
-
111
- <DialogFooter>
112
- <Button
113
- onClick={() => handleOpenChange(false)}
114
- variant="outline"
115
- disabled={isGeneratingAnimation}
116
- >
117
- Cancel
118
- </Button>
119
- <Button
120
- onClick={handleAnimationRequest}
121
- disabled={isGeneratingAnimation || !urdfContent}
122
- className="ml-2"
123
- >
124
- {isGeneratingAnimation ? (
125
- <>
126
- <Loader className="mr-2 h-4 w-4 animate-spin" />
127
- Generating...
128
- </>
129
- ) : (
130
- "Apply Animation"
131
- )}
132
- </Button>
133
- </DialogFooter>
134
- </DialogContent>
135
- </Dialog>
136
- );
137
- };
138
-
139
- export default AnimationDialog;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/components/Carousel.tsx DELETED
@@ -1,175 +0,0 @@
1
- import React, { useRef, useState, useEffect } from "react";
2
- import { ContentItem } from "../lib/types";
3
- import { useNavigate } from "react-router-dom";
4
- import { ChevronLeft, ChevronRight, Star } from "lucide-react";
5
- import { cn } from "@/lib/utils";
6
- import { useIsMobile } from "@/hooks/use-mobile";
7
- import { useMyList } from "@/hooks/use-my-list";
8
- import { supabase } from "@/integrations/supabase/client";
9
- import { useUrdf } from "@/hooks/useUrdf";
10
-
11
- interface CarouselProps {
12
- title: string;
13
- items: ContentItem[];
14
- className?: string;
15
- }
16
-
17
- const Carousel: React.FC<CarouselProps> = ({ title, items, className }) => {
18
- const carouselRef = useRef<HTMLDivElement>(null);
19
- const navigate = useNavigate();
20
- const isMobile = useIsMobile();
21
- const { myList, addToMyList, removeFromMyList, isInMyList } = useMyList();
22
- const [imageUrls, setImageUrls] = useState<Record<string, string>>({});
23
- const { urdfProcessor, processUrdfFiles } = useUrdf();
24
-
25
- // Fetch image URLs for all items
26
- useEffect(() => {
27
- const fetchImages = async () => {
28
- const urls: Record<string, string> = {};
29
-
30
- for (const item of items) {
31
- if (
32
- item.imageUrl &&
33
- !item.imageUrl.startsWith("http") &&
34
- !item.imageUrl.startsWith("data:")
35
- ) {
36
- try {
37
- // Get public URL for the image
38
- const { data, error } = await supabase.storage
39
- .from("urdf-images")
40
- .createSignedUrl(item.imageUrl, 3600); // 1 hour expiry
41
-
42
- if (data && !error) {
43
- urls[item.id] = data.signedUrl;
44
- } else {
45
- // Fallback to placeholder
46
- urls[item.id] = "/placeholder.svg";
47
- }
48
- } catch (error) {
49
- console.error(`Error fetching image for ${item.id}:`, error);
50
- urls[item.id] = "/placeholder.svg";
51
- }
52
- } else {
53
- // For URLs that are already full URLs or data URIs
54
- urls[item.id] = item.imageUrl || "/placeholder.svg";
55
- }
56
- }
57
-
58
- setImageUrls(urls);
59
- };
60
-
61
- if (items.length > 0) {
62
- fetchImages();
63
- }
64
- }, [items]);
65
-
66
- const handleScrollLeft = () => {
67
- if (carouselRef.current) {
68
- const scrollAmount = carouselRef.current.offsetWidth / 4.1;
69
- carouselRef.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
70
- }
71
- };
72
-
73
- const handleScrollRight = () => {
74
- if (carouselRef.current) {
75
- const scrollAmount = carouselRef.current.offsetWidth / 4.1;
76
- carouselRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
77
- }
78
- };
79
-
80
- const handleItemClick = async (item: ContentItem) => {
81
- // Only navigate to the content detail page, let the detail page handle loading
82
- navigate(`/content/${item.id}`);
83
-
84
- // We've removed the URDF loading here to prevent duplication with ContentDetail's loading
85
- };
86
-
87
- const handleStarClick = (e: React.MouseEvent, item: ContentItem) => {
88
- e.stopPropagation(); // Prevent navigation on star click
89
-
90
- if (isInMyList(item.id)) {
91
- removeFromMyList(item.id);
92
- } else {
93
- addToMyList(item);
94
- }
95
- };
96
-
97
- // If no items, don't render the carousel
98
- if (items.length === 0) return null;
99
-
100
- return (
101
- <div className={cn("my-5", className)}>
102
- <div className="relative group">
103
- <div
104
- ref={carouselRef}
105
- className="carousel-container flex items-center gap-2 overflow-x-auto py-2 px-4 scroll-smooth"
106
- >
107
- {items.map((item) => (
108
- <div
109
- key={item.id}
110
- className="carousel-item flex-shrink-0 cursor-pointer relative hover:z-10"
111
- style={{
112
- width: "calc(100% / 4.1)",
113
- }}
114
- onClick={() => handleItemClick(item)}
115
- >
116
- <div className="relative rounded-md w-full h-full group/item">
117
- {/* Image container with darker overlay on hover */}
118
- <div className="rounded-md overflow-hidden w-full h-full bg-black">
119
- <img
120
- src={imageUrls[item.id] || "/placeholder.svg"}
121
- alt={item.title}
122
- className="w-full h-full object-cover rounded-md transition-all duration-300 group-hover/item:brightness-90"
123
- style={{
124
- aspectRatio: "0.8",
125
- }}
126
- />
127
- </div>
128
- {/* Star button */}
129
- <div
130
- className="absolute top-4 right-4 p-2 z-20 invisible group-hover/item:visible"
131
- onClick={(e) => handleStarClick(e, item)}
132
- >
133
- <Star
134
- size={24}
135
- className={cn(
136
- "transition-colors duration-300",
137
- isInMyList(item.id)
138
- ? "fill-yellow-400 text-yellow-400"
139
- : "text-white hover:text-yellow-400"
140
- )}
141
- />
142
- </div>
143
- {/* Title overlay - visible on hover without gradient */}
144
- <div className="absolute bottom-4 left-4 opacity-0 group-hover/item:opacity-100 transition-opacity duration-300">
145
- <h3 className="text-gray-400 text-6xl font-bold drop-shadow-xl">
146
- {item.title}
147
- </h3>
148
- </div>
149
- </div>
150
- </div>
151
- ))}
152
- </div>
153
-
154
- {/* Scroll buttons - changed to always visible */}
155
- <button
156
- onClick={handleScrollLeft}
157
- className="absolute left-0 top-1/2 -translate-y-1/2 bg-black text-white p-1 rounded-full z-40"
158
- aria-label="Scroll left"
159
- >
160
- <ChevronLeft size={24} />
161
- </button>
162
-
163
- <button
164
- onClick={handleScrollRight}
165
- className="absolute right-0 top-1/2 -translate-y-1/2 bg-black text-white p-1 rounded-full z-40"
166
- aria-label="Scroll right"
167
- >
168
- <ChevronRight size={24} />
169
- </button>
170
- </div>
171
- </div>
172
- );
173
- };
174
-
175
- export default Carousel;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/components/ChatWindow.tsx DELETED
@@ -1,309 +0,0 @@
1
- import React, { useState, useRef, useEffect } from "react";
2
- import { contentItems } from "../../public/data/content";
3
- import { Send } from "lucide-react";
4
- import { useNavigate } from "react-router-dom";
5
- // Commenting out the backend API integration for now
6
- // import { useChatApi, ChatMessage } from "../hooks/useChatApi";
7
-
8
- // Define the ChatMessage interface locally since we're not importing it
9
- interface ChatMessage {
10
- sender: string;
11
- text: string;
12
- imageUrl?: string;
13
- robotId?: string; // Add robotId to track which robot this message refers to
14
- }
15
-
16
- // Mock data for quadruped robots
17
- const quadrupedRobots = [
18
- {
19
- name: "Go2",
20
- description:
21
- "Go1 is a lightweight, agile quadruped companion robot. It can follow you around, carry small items, and navigate complex indoor and outdoor environments. Perfect for research, education, or as a high-tech assistant in various settings.",
22
- imageUrl:
23
- "https://mizajlqhooderueazvnp.supabase.co/storage/v1/object/public/robotpicturesbucket/go2_description.png",
24
- id: "0f041a8f-88cf-4b9c-93ef-a30e8fe2fdb1",
25
- },
26
- {
27
- name: "ANYmal",
28
- description:
29
- "ANYmal is a rugged quadrupedal robot designed for autonomous operation in challenging environments. With its sophisticated perception systems, it excels at inspection and monitoring tasks in industrial settings, even in hazardous areas unsafe for humans.",
30
- imageUrl:
31
- "https://mizajlqhooderueazvnp.supabase.co/storage/v1/object/public/robotpicturesbucket/anymal_b_description.png",
32
- id: "68e827db-4035-4ae0-a43c-158b610e21d5",
33
- },
34
- ];
35
-
36
- // Function to get a random wait time to simulate thinking
37
- const getRandomWaitTime = () => Math.floor(Math.random() * 150) + 50; // 50-200ms
38
-
39
- const ChatWindow: React.FC = () => {
40
- const [messages, setMessages] = useState<ChatMessage[]>([]);
41
- const [input, setInput] = useState("");
42
- const [isLoading, setIsLoading] = useState(false);
43
- const chatContainerRef = useRef<HTMLDivElement>(null);
44
- const navigate = useNavigate();
45
-
46
- // Commenting out the backend API integration
47
- // const { sendMessage, streamResponse, isLoading, error } = useChatApi();
48
-
49
- const handleSend = async () => {
50
- if (input.trim()) {
51
- const userMessage = input.trim();
52
-
53
- // Add user message to chat
54
- setMessages((prevMessages) => [
55
- ...prevMessages,
56
- {
57
- sender: "User",
58
- text: userMessage,
59
- },
60
- ]);
61
-
62
- setInput("");
63
- setIsLoading(true);
64
-
65
- // Add empty bot message that will be filled with streaming response
66
- setMessages((prevMessages) => [
67
- ...prevMessages,
68
- {
69
- sender: "Bot",
70
- text: "",
71
- },
72
- ]);
73
-
74
- // Use our mock response generator instead of the API
75
- await generateMockResponse(userMessage);
76
- }
77
- };
78
-
79
- // Generate a fake response about quadruped robots
80
- const generateMockResponse = async (userMessage: string) => {
81
- try {
82
- // Choose a relevant robot based on keywords in user message
83
- let selectedRobot;
84
- const userMessageLower = userMessage.toLowerCase();
85
-
86
- if (
87
- userMessageLower.includes("agile") ||
88
- userMessageLower.includes("mobility")
89
- ) {
90
- selectedRobot = quadrupedRobots.find((robot) => robot.name === "Go2");
91
- } else if (
92
- userMessageLower.includes("companion") ||
93
- userMessageLower.includes("small")
94
- ) {
95
- selectedRobot = quadrupedRobots.find((robot) => robot.name === "Go2");
96
- } else if (
97
- userMessageLower.includes("inspect") ||
98
- userMessageLower.includes("monitor") ||
99
- userMessageLower.includes("industrial")
100
- ) {
101
- selectedRobot = quadrupedRobots.find(
102
- (robot) => robot.name === "ANYmal"
103
- );
104
- } else {
105
- // If no specific match, pick a random robot
106
- const randomIndex = Math.floor(Math.random() * quadrupedRobots.length);
107
- selectedRobot = quadrupedRobots[randomIndex];
108
- }
109
-
110
- // Generate response text
111
- const responseIntro = getResponseIntro(userMessage);
112
- const fullResponse = `${responseIntro} ${selectedRobot.name}. ${selectedRobot.description}`;
113
-
114
- // Simulate streaming by adding words one by one with small delays
115
- const words = fullResponse.split(" ");
116
- for (const word of words) {
117
- await new Promise((resolve) =>
118
- setTimeout(resolve, getRandomWaitTime())
119
- );
120
-
121
- setMessages((prevMessages) => {
122
- const updatedMessages = [...prevMessages];
123
- const lastMessage = updatedMessages[updatedMessages.length - 1];
124
-
125
- if (lastMessage.sender === "Bot") {
126
- lastMessage.text += word + " ";
127
- }
128
-
129
- return updatedMessages;
130
- });
131
- }
132
-
133
- // Add the image and robot ID at the end
134
- await new Promise((resolve) => setTimeout(resolve, 300));
135
- setMessages((prevMessages) => {
136
- const updatedMessages = [...prevMessages];
137
- const lastMessage = updatedMessages[updatedMessages.length - 1];
138
-
139
- if (lastMessage.sender === "Bot") {
140
- lastMessage.imageUrl = selectedRobot.imageUrl;
141
- lastMessage.robotId = selectedRobot.id; // Store the robot ID for navigation
142
- }
143
-
144
- return updatedMessages;
145
- });
146
- } catch (error) {
147
- console.error("Error generating mock response:", error);
148
- // Handle error by providing a generic fallback
149
- setMessages((prevMessages) => {
150
- const updatedMessages = [...prevMessages];
151
- const lastMessage = updatedMessages[updatedMessages.length - 1];
152
-
153
- if (lastMessage.sender === "Bot") {
154
- lastMessage.text =
155
- "I'm sorry, I couldn't process your request. Please try asking about robots in a different way.";
156
- }
157
-
158
- return updatedMessages;
159
- });
160
- } finally {
161
- setIsLoading(false);
162
- }
163
- };
164
-
165
- // Navigate to content detail page
166
- const navigateToRobotDetail = (robotId: string) => {
167
- if (robotId) {
168
- navigate(`/content/${robotId}`);
169
- }
170
- };
171
-
172
- // Get varied intro phrases to make responses seem more natural
173
- const getResponseIntro = (userMessage: string) => {
174
- const introOptions = [
175
- "Based on your query, I recommend",
176
- "I think you might be interested in",
177
- "A great quadruped robot for your needs is",
178
- "You should check out",
179
- "Have you considered",
180
- "I'd suggest looking at",
181
- ];
182
-
183
- const randomIndex = Math.floor(Math.random() * introOptions.length);
184
- return introOptions[randomIndex];
185
- };
186
-
187
- // Fallback response is no longer needed since we're using mock responses
188
-
189
- useEffect(() => {
190
- if (chatContainerRef.current) {
191
- setTimeout(() => {
192
- chatContainerRef.current!.scrollTop =
193
- chatContainerRef.current!.scrollHeight;
194
- }, 50); // Add a slight delay to ensure the DOM is updated
195
- }
196
- }, [messages]);
197
-
198
- return (
199
- <div className="bg-[rgba(10,10,20,0.8)] backdrop-blur-lg border border-white/10 text-white p-6 rounded-lg shadow-2xl">
200
- <div
201
- ref={chatContainerRef}
202
- className="overflow-y-auto mb-6 scrollbar-none"
203
- style={{ maxHeight: "500px" }}
204
- >
205
- {messages.length === 0 && (
206
- <div className="text-center py-8 text-gray-400 italic">
207
- Ask me about robots, and I'll help you find the perfect fit for your
208
- needs. NOTE: For this demo, I only know about Go2 and ANYmal + don't
209
- want to use up all my LLM credits.
210
- </div>
211
- )}
212
-
213
- {messages.map((message, index) => (
214
- <div
215
- key={index}
216
- className={`mb-4 ${
217
- message.sender === "User" ? "text-right" : "text-left"
218
- } animate-fade-in`}
219
- >
220
- <div className="flex items-start gap-2 mb-1">
221
- <span
222
- className={`text-sm font-semibold ${
223
- message.sender === "User" ? "ml-auto" : ""
224
- }`}
225
- >
226
- {message.sender === "User" ? "You" : "Assistant"}
227
- </span>
228
- </div>
229
-
230
- <span
231
- className={`inline-block px-4 py-3 rounded-lg ${
232
- message.sender === "User"
233
- ? "bg-blue-600 text-white"
234
- : "bg-gray-700/70"
235
- }`}
236
- style={{ whiteSpace: "pre-wrap" }}
237
- >
238
- {message.text}
239
- </span>
240
-
241
- {message.imageUrl && (
242
- <div
243
- className="mt-3 transition-all duration-300 hover:scale-105 cursor-pointer"
244
- onClick={() =>
245
- message.robotId && navigateToRobotDetail(message.robotId)
246
- }
247
- title="Click to view robot details"
248
- >
249
- <img
250
- src={message.imageUrl}
251
- alt="Robot"
252
- className="rounded-lg w-auto shadow-lg border border-white/10 hover:border-white/30"
253
- style={{ maxHeight: "260px" }}
254
- />
255
- <div className="text-xs text-blue-300 mt-1 text-center">
256
- Click to view details
257
- </div>
258
- </div>
259
- )}
260
- </div>
261
- ))}
262
-
263
- {isLoading &&
264
- messages.length > 0 &&
265
- !messages[messages.length - 1].text && (
266
- <div className="text-left animate-pulse">
267
- <span className="inline-block px-4 py-3 rounded-lg bg-gray-700/50">
268
- <div className="flex space-x-1">
269
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
270
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-75"></div>
271
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-150"></div>
272
- </div>
273
- </span>
274
- </div>
275
- )}
276
- </div>
277
-
278
- <div className="flex items-center relative">
279
- <textarea
280
- value={input}
281
- onChange={(e) => setInput(e.target.value)}
282
- rows={1}
283
- placeholder="Type your message..."
284
- onInput={(e) => {
285
- const target = e.target as HTMLTextAreaElement;
286
- target.style.height = "auto";
287
- target.style.height = `${target.scrollHeight}px`;
288
- }}
289
- onKeyDown={(e) => {
290
- if (e.key === "Enter" && !e.shiftKey) {
291
- e.preventDefault();
292
- handleSend();
293
- }
294
- }}
295
- className="flex-1 px-4 py-4 rounded-lg bg-gray-800/80 text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-white/25 resize-none overflow-hidden pr-12 border border-white/5"
296
- />
297
- <button
298
- onClick={handleSend}
299
- className="absolute right-2 bottom-2 p-2.5 bg-blue-600 rounded-full hover:bg-blue-700 transition-colors disabled:opacity-50"
300
- disabled={isLoading}
301
- >
302
- <Send size={18} className="text-white" />
303
- </button>
304
- </div>
305
- </div>
306
- );
307
- };
308
-
309
- export default ChatWindow;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/components/ContentCarousel.tsx DELETED
@@ -1,29 +0,0 @@
1
- import React from "react";
2
- import Carousel from "./Carousel";
3
- import { Category } from "../lib/types";
4
- import { useUrdfsByCategory } from "@/hooks/useUrdfData";
5
- import { Skeleton } from "@/components/ui/skeleton";
6
-
7
- interface ContentCarouselProps {
8
- category: Category;
9
- }
10
-
11
- const ContentCarousel: React.FC<ContentCarouselProps> = ({ category }) => {
12
- const { data: filteredItems, isLoading } = useUrdfsByCategory(category.id);
13
-
14
- if (isLoading) {
15
- return (
16
- <div className="relative w-full">
17
- <Skeleton className="h-64 w-full" />
18
- </div>
19
- );
20
- }
21
-
22
- return filteredItems.length > 0 ? (
23
- <div className="relative w-full">
24
- <Carousel title={category.name} items={filteredItems} />
25
- </div>
26
- ) : null;
27
- };
28
-
29
- export default ContentCarousel;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/components/Header.tsx DELETED
@@ -1,150 +0,0 @@
1
- import React from "react";
2
- import { Link, useLocation } from "react-router-dom";
3
- import { Compass, User, Gamepad2 } from "lucide-react";
4
- import {
5
- DropdownMenu,
6
- DropdownMenuContent,
7
- DropdownMenuItem,
8
- DropdownMenuLabel,
9
- DropdownMenuSeparator,
10
- DropdownMenuTrigger,
11
- } from "@/components/ui/dropdown-menu";
12
- // import { useProjects } from '../hooks/use-projects';
13
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
14
- import { cn } from "@/lib/utils";
15
-
16
- const Header: React.FC = () => {
17
- // const { projects } = useProjects();
18
- const location = useLocation();
19
-
20
- const isActive = (path: string) => {
21
- return location.pathname === path;
22
- };
23
-
24
- return (
25
- <header className="backdrop-blur-md py-4 px-6 flex items-center justify-between sticky top-0 z-50 shadow-lg border-b border-white/5 bg-zinc-950">
26
- <div className="flex items-center">
27
- <Link to="/" className="flex items-center mr-8 group">
28
- <div className="w-10 h-10 overflow-hidden">
29
- <img
30
- src="/lovable-uploads/166630d9-f089-4678-a27c-7a00882f5ed0.png"
31
- alt="Spiral Logo"
32
- className="w-full h-full object-contain transition-transform group-hover:scale-110"
33
- />
34
- </div>
35
- <span className="ml-2 text-white font-bold text-xl tracking-wider group-hover:text-blue-400 transition-colors">
36
- QUALIA
37
- </span>
38
- </Link>
39
- <nav className="hidden md:flex space-x-8">
40
- {/* <Link
41
- to="/"
42
- className="flex items-center gap-2 text-white/80 hover:text-white transition-colors text-sm"
43
- >
44
- <Home size={16} />
45
- <span>Home</span>
46
- </Link> */}
47
- <Link
48
- to="/explore"
49
- className={cn(
50
- "flex items-center gap-2 transition-colors text-sm relative",
51
- isActive("/explore")
52
- ? "text-blue-400 font-medium"
53
- : "text-white/80 hover:text-white"
54
- )}
55
- >
56
- <Compass size={16} />
57
- <span>Explore</span>
58
- {isActive("/explore") && (
59
- <div className="absolute -bottom-2 left-0 w-full h-0.5 bg-blue-400 rounded-full" />
60
- )}
61
- </Link>
62
- {/* <Link
63
- to="/my-list"
64
- className="flex items-center gap-2 text-white/80 hover:text-white transition-colors text-sm"
65
- >
66
- <Archive size={16} />
67
- <span>Projects</span>
68
- </Link> */}
69
- <Link
70
- to="/playground"
71
- className={cn(
72
- "flex items-center gap-2 transition-colors text-sm relative",
73
- isActive("/playground")
74
- ? "text-blue-400 font-medium"
75
- : "text-white/80 hover:text-white"
76
- )}
77
- >
78
- <div className="flex items-center gap-2">
79
- <Gamepad2 size={16} />
80
- <span>Playground</span>
81
- </div>
82
- {isActive("/playground") && (
83
- <div className="absolute -bottom-2 left-0 w-full h-0.5 bg-blue-400 rounded-full" />
84
- )}
85
- </Link>
86
- </nav>
87
- </div>
88
- <div className="flex items-center space-x-6">
89
- <DropdownMenu>
90
- <DropdownMenuTrigger asChild>
91
- <div className="w-9 h-9 bg-gradient-to-br from-blue-500 to-purple-600 rounded-md transition-transform hover:scale-110 cursor-pointer flex items-center justify-center">
92
- <Avatar className="w-full h-full">
93
- <AvatarImage src="" alt="Profile" />
94
- <AvatarFallback className="bg-transparent text-white">
95
- <User size={18} />
96
- </AvatarFallback>
97
- </Avatar>
98
- </div>
99
- </DropdownMenuTrigger>
100
- <DropdownMenuContent
101
- align="end"
102
- className="w-56 bg-zinc-900 border-zinc-800 text-white"
103
- >
104
- <DropdownMenuLabel className="font-normal">
105
- <div className="flex flex-col space-y-1">
106
- <p className="text-sm font-medium leading-none">Victor</p>
107
- <p className="text-xs leading-none text-zinc-400">
108
109
- </p>
110
- </div>
111
- </DropdownMenuLabel>
112
- <DropdownMenuSeparator className="bg-zinc-800" />
113
- <DropdownMenuLabel>Your Projects</DropdownMenuLabel>
114
-
115
- {/* {projects.length > 0 ? (
116
- <>
117
- {projects.slice(0, 3).map(project => (
118
- <DropdownMenuItem key={project.id} className="cursor-pointer hover:bg-zinc-800" asChild>
119
- <Link to={`/my-list?project=${project.id}`} className="w-full">
120
- <span className="truncate">{project.name}</span>
121
- </Link>
122
- </DropdownMenuItem>
123
- ))}
124
- {projects.length > 3 && (
125
- <DropdownMenuItem className="text-xs text-zinc-400 hover:bg-zinc-800" asChild>
126
- <Link to="/my-list">View all projects</Link>
127
- </DropdownMenuItem>
128
- )}
129
- </>
130
- ) : (
131
- <DropdownMenuItem className="text-zinc-400 cursor-default">
132
- No projects yet
133
- </DropdownMenuItem>
134
- )} */}
135
-
136
- <DropdownMenuSeparator className="bg-zinc-800" />
137
- <DropdownMenuItem
138
- className="cursor-pointer hover:bg-zinc-800"
139
- asChild
140
- >
141
- <Link to="/my-list">Manage Projects</Link>
142
- </DropdownMenuItem>
143
- </DropdownMenuContent>
144
- </DropdownMenu>
145
- </div>
146
- </header>
147
- );
148
- };
149
-
150
- export default Header;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/components/TeamSection.tsx DELETED
@@ -1,83 +0,0 @@
1
- import React from "react";
2
- import { Card, CardContent } from "@/components/ui/card";
3
- import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
4
-
5
- interface TeamMember {
6
- id: string;
7
- name: string;
8
- role: string;
9
- imageUrl: string;
10
- bio?: string;
11
- }
12
-
13
- const teamMembers: TeamMember[] = [
14
- {
15
- id: "1",
16
- name: "Max",
17
- role: "Co-founder",
18
- imageUrl: "/lovable-uploads/07251c16-3e3c-4c40-8450-c6c17f291e00.png",
19
- bio: "Cracked engineer",
20
- },
21
- {
22
- id: "2",
23
- name: "Ludvig",
24
- role: "Co-founder",
25
- imageUrl: "/lovable-uploads/e6811f8e-3c1e-4a80-80e5-4b82f4704aec.png",
26
- bio: "Cracked engineer",
27
- },
28
- {
29
- id: "3",
30
- name: "Victor",
31
- role: "Co-founder",
32
- imageUrl: "/lovable-uploads/21f0e012-4ef0-4db0-a1e2-aa5205c8400e.png",
33
- bio: "Cracked engineer",
34
- },
35
- {
36
- id: "4",
37
- name: "Fabian",
38
- role: "Co-founder",
39
- imageUrl: "/lovable-uploads/e6f4bf50-ed44-4ce7-9fab-ff8a9476b584.png",
40
- bio: "(Almost) cracked engineer",
41
- },
42
- ];
43
-
44
- const TeamSection: React.FC = () => {
45
- return (
46
- <section className="py-16 relative z-10">
47
- <div className="container mx-auto px-4">
48
- <h2 className="text-5xl font-bold mb-12 text-center text-netflix-text text-glow">
49
- Our Team
50
- </h2>
51
-
52
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
53
- {teamMembers.map((member) => (
54
- <Card
55
- key={member.id}
56
- className="glass-panel hover:shadow-glow transition-all duration-300 transform hover:-translate-y-1"
57
- >
58
- <CardContent className="pt-6 text-center">
59
- <Avatar className="h-32 w-32 mx-auto mb-4 border-2 border-white/20">
60
- <AvatarImage
61
- src={member.imageUrl}
62
- alt={member.name}
63
- className="filter grayscale"
64
- />
65
- <AvatarFallback className="text-2xl">
66
- {member.name.charAt(0)}
67
- </AvatarFallback>
68
- </Avatar>
69
- <h3 className="text-xl font-bold mb-1">{member.name}</h3>
70
- <p className="text-netflix-lightText mb-3">{member.role}</p>
71
- {member.bio && (
72
- <p className="text-sm text-netflix-text2">{member.bio}</p>
73
- )}
74
- </CardContent>
75
- </Card>
76
- ))}
77
- </div>
78
- </div>
79
- </section>
80
- );
81
- };
82
-
83
- export default TeamSection;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/hooks/use-my-list.tsx DELETED
@@ -1,74 +0,0 @@
1
- import {
2
- useState,
3
- useEffect,
4
- createContext,
5
- useContext,
6
- ReactNode,
7
- } from "react";
8
- import { ContentItem } from "../lib/types";
9
-
10
- interface MyListContextType {
11
- myList: ContentItem[];
12
- addToMyList: (item: ContentItem) => void;
13
- removeFromMyList: (id: string) => void;
14
- isInMyList: (id: string) => boolean;
15
- }
16
-
17
- const MyListContext = createContext<MyListContextType | undefined>(undefined);
18
-
19
- export const MyListProvider: React.FC<{ children: ReactNode }> = ({
20
- children,
21
- }) => {
22
- const [myList, setMyList] = useState<ContentItem[]>([]);
23
-
24
- // Load saved items from local storage
25
- useEffect(() => {
26
- const savedList = localStorage.getItem("myList");
27
- if (savedList) {
28
- try {
29
- setMyList(JSON.parse(savedList));
30
- } catch (error) {
31
- console.error("Failed to parse saved list:", error);
32
- }
33
- }
34
- }, []);
35
-
36
- // Save items to local storage when list changes
37
- useEffect(() => {
38
- localStorage.setItem("myList", JSON.stringify(myList));
39
- }, [myList]);
40
-
41
- const addToMyList = (item: ContentItem) => {
42
- setMyList((prev) => {
43
- // Only add if not already in list
44
- if (!prev.some((listItem) => listItem.id === item.id)) {
45
- return [...prev, item];
46
- }
47
- return prev;
48
- });
49
- };
50
-
51
- const removeFromMyList = (id: string) => {
52
- setMyList((prev) => prev.filter((item) => item.id !== id));
53
- };
54
-
55
- const isInMyList = (id: string) => {
56
- return myList.some((item) => item.id === id);
57
- };
58
-
59
- return (
60
- <MyListContext.Provider
61
- value={{ myList, addToMyList, removeFromMyList, isInMyList }}
62
- >
63
- {children}
64
- </MyListContext.Provider>
65
- );
66
- };
67
-
68
- export const useMyList = () => {
69
- const context = useContext(MyListContext);
70
- if (context === undefined) {
71
- throw new Error("useMyList must be used within a MyListProvider");
72
- }
73
- return context;
74
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/hooks/useChatApi.ts DELETED
@@ -1,129 +0,0 @@
1
- import { useState, useCallback, useEffect } from "react";
2
- import { useMutation } from "@tanstack/react-query";
3
- import { v4 as uuidv4 } from "uuid";
4
-
5
- // Generate a conversation ID to help backend keep track of the chat history
6
- const getConversationId = () => {
7
- const storedId = localStorage.getItem("chat_conversation_id");
8
- if (storedId) return storedId;
9
-
10
- const newId = uuidv4();
11
- localStorage.setItem("chat_conversation_id", newId);
12
- return newId;
13
- };
14
-
15
- export interface ChatMessage {
16
- sender: string;
17
- text: string;
18
- imageUrl?: string;
19
- }
20
-
21
- export const useChatApi = () => {
22
- const [conversationId] = useState(getConversationId);
23
- const [isStreaming, setIsStreaming] = useState(false);
24
-
25
- // Use mutation for the API call
26
- const chatMutation = useMutation({
27
- mutationFn: async (prompt: string) => {
28
- const controller = new AbortController();
29
- const signal = controller.signal;
30
-
31
- const response = await fetch("http://127.0.0.1:8000/prompt", {
32
- method: "POST",
33
- headers: {
34
- "Content-Type": "application/json",
35
- },
36
- body: JSON.stringify({
37
- session_id: conversationId,
38
- prompt: prompt,
39
- }),
40
- signal,
41
- });
42
-
43
- if (!response.ok) {
44
- throw new Error("Network response was not ok");
45
- }
46
-
47
- return { response, controller };
48
- },
49
- });
50
-
51
- // Process the streaming response
52
- const streamResponse = useCallback(
53
- async (
54
- response: Response,
55
- onChunk: (chunk: string) => void,
56
- onImage?: (imageUrl: string) => void
57
- ) => {
58
- if (!response.body) {
59
- throw new Error("Response body is null");
60
- }
61
-
62
- setIsStreaming(true);
63
-
64
- const reader = response.body.getReader();
65
- const decoder = new TextDecoder();
66
- let buffer = "";
67
-
68
- try {
69
- while (true) {
70
- const { done, value } = await reader.read();
71
-
72
- if (done) {
73
- break;
74
- }
75
-
76
- // Decode the chunk
77
- const text = decoder.decode(value, { stream: true });
78
- buffer += text;
79
-
80
- // Process SSE format: "data: {json}\n\n"
81
- const lines = buffer.split("\n\n");
82
- buffer = lines.pop() || "";
83
-
84
- for (const line of lines) {
85
- if (line.startsWith("data: ")) {
86
- const jsonStr = line.slice(6);
87
- try {
88
- const data = JSON.parse(jsonStr);
89
-
90
- // Check if there's text content
91
- if (data.text) {
92
- onChunk(data.text);
93
- }
94
-
95
- // Check if there's an image URL
96
- if (data.image_url) {
97
- onImage?.(data.image_url);
98
- }
99
- } catch (e) {
100
- console.error("Error parsing SSE JSON:", e);
101
- }
102
- }
103
- }
104
- }
105
- } catch (error) {
106
- console.error("Error reading stream:", error);
107
- } finally {
108
- setIsStreaming(false);
109
- }
110
- },
111
- []
112
- );
113
-
114
- // Cleanup function to abort any pending requests
115
- useEffect(() => {
116
- return () => {
117
- if (chatMutation.data?.controller) {
118
- chatMutation.data.controller.abort();
119
- }
120
- };
121
- }, [chatMutation.data]);
122
-
123
- return {
124
- sendMessage: chatMutation.mutate,
125
- streamResponse,
126
- isLoading: chatMutation.isPending || isStreaming,
127
- error: chatMutation.error,
128
- };
129
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/hooks/useCreateAnimation.ts DELETED
@@ -1,114 +0,0 @@
1
- import { useState } from "react";
2
- import { supabase } from "@/lib/supabase";
3
- import { AnimationRequest, RobotAnimationConfig } from "@/lib/types";
4
- import { toast } from "sonner";
5
-
6
- export const useCreateAnimation = () => {
7
- const [isLoading, setIsLoading] = useState(false);
8
- const [error, setError] = useState<string | null>(null);
9
- const [animation, setAnimation] = useState<RobotAnimationConfig | null>(null);
10
-
11
- const createAnimation = async (
12
- request: AnimationRequest
13
- ): Promise<RobotAnimationConfig | null> => {
14
- const requestId = `anim-${Date.now()}`; // Generate unique ID for tracking
15
- console.log(`[${requestId}] 🚀 Animation Generator: Starting request`);
16
- console.log(`[${requestId}] 🔍 Description: "${request.description}"`);
17
-
18
- setIsLoading(true);
19
- setError(null);
20
-
21
- const startTime = performance.now();
22
-
23
- try {
24
- console.log(
25
- `[${requestId}] 📡 Calling Supabase edge function "create-animation"...`
26
- );
27
-
28
- const { data, error } = await supabase.functions.invoke(
29
- "create-animation",
30
- {
31
- body: {
32
- robotName: request.robotName,
33
- urdfContent: request.urdfContent,
34
- description: request.description,
35
- },
36
- }
37
- );
38
-
39
- if (error) {
40
- console.error(`[${requestId}] ❌ Supabase function error:`, error);
41
-
42
- // Format the error message for display
43
- const errorMessage = error.message || "Unknown error occurred";
44
- setError(errorMessage);
45
-
46
- toast.error("Animation Generation Failed", {
47
- description: errorMessage.includes("non-2xx status code")
48
- ? "The animation generator encountered a server error."
49
- : errorMessage,
50
- duration: 5000,
51
- });
52
-
53
- throw new Error(errorMessage);
54
- }
55
-
56
- const endTime = performance.now();
57
- console.log(
58
- `[${requestId}] ✅ Edge function responded in ${(
59
- endTime - startTime
60
- ).toFixed(2)}ms`
61
- );
62
-
63
- if (!data) {
64
- throw new Error("No data returned from edge function");
65
- }
66
-
67
- // Quick validation of minimum required structure
68
- if (
69
- !data.joints ||
70
- !Array.isArray(data.joints) ||
71
- data.joints.length === 0
72
- ) {
73
- console.error(`[${requestId}] ⚠️ Invalid animation data:`, data);
74
- throw new Error(
75
- "Invalid animation configuration: Missing joint animations"
76
- );
77
- }
78
-
79
- console.log(
80
- `[${requestId}] 🤖 Animation generated with ${data.joints.length} joint(s)`
81
- );
82
-
83
- // Store the animation data
84
- setAnimation(data);
85
- return data;
86
- } catch (err) {
87
- const errorMessage =
88
- err instanceof Error ? err.message : "Unknown error occurred";
89
- const endTime = performance.now();
90
-
91
- console.error(
92
- `[${requestId}] ❌ Error generating animation after ${(
93
- endTime - startTime
94
- ).toFixed(2)}ms:`,
95
- err
96
- );
97
-
98
- setError(errorMessage);
99
- return null;
100
- } finally {
101
- setIsLoading(false);
102
- console.log(`[${requestId}] 🏁 Animation request completed`);
103
- }
104
- };
105
-
106
- return {
107
- createAnimation,
108
- animation,
109
- isLoading,
110
- error,
111
- clearAnimation: () => setAnimation(null),
112
- clearError: () => setError(null),
113
- };
114
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/src/integrations/supabase/types.ts DELETED
@@ -1,330 +0,0 @@
1
- export type Json =
2
- | string
3
- | number
4
- | boolean
5
- | null
6
- | { [key: string]: Json | undefined }
7
- | Json[]
8
-
9
- export type Database = {
10
- public: {
11
- Tables: {
12
- urdf: {
13
- Row: {
14
- dof: number | null
15
- has_manipulator: boolean | null
16
- id: string
17
- image_uri: string | null
18
- joints: Json | null
19
- links: Json | null
20
- maker: string | null
21
- manipulators: string[] | null
22
- name: string
23
- num_joints: number | null
24
- num_links: number | null
25
- num_manipulators: number | null
26
- summary: string | null
27
- tags: string[] | null
28
- total_mass: number | null
29
- type: string | null
30
- urdf_uri: string | null
31
- }
32
- Insert: {
33
- dof?: number | null
34
- has_manipulator?: boolean | null
35
- id?: string
36
- image_uri?: string | null
37
- joints?: Json | null
38
- links?: Json | null
39
- maker?: string | null
40
- manipulators?: string[] | null
41
- name: string
42
- num_joints?: number | null
43
- num_links?: number | null
44
- num_manipulators?: number | null
45
- summary?: string | null
46
- tags?: string[] | null
47
- total_mass?: number | null
48
- type?: string | null
49
- urdf_uri?: string | null
50
- }
51
- Update: {
52
- dof?: number | null
53
- has_manipulator?: boolean | null
54
- id?: string
55
- image_uri?: string | null
56
- joints?: Json | null
57
- links?: Json | null
58
- maker?: string | null
59
- manipulators?: string[] | null
60
- name?: string
61
- num_joints?: number | null
62
- num_links?: number | null
63
- num_manipulators?: number | null
64
- summary?: string | null
65
- tags?: string[] | null
66
- total_mass?: number | null
67
- type?: string | null
68
- urdf_uri?: string | null
69
- }
70
- Relationships: []
71
- }
72
- urdf_embeddings: {
73
- Row: {
74
- embeddings: string
75
- id: string
76
- name: string | null
77
- summary: string | null
78
- }
79
- Insert: {
80
- embeddings: string
81
- id?: string
82
- name?: string | null
83
- summary?: string | null
84
- }
85
- Update: {
86
- embeddings?: string
87
- id?: string
88
- name?: string | null
89
- summary?: string | null
90
- }
91
- Relationships: [
92
- {
93
- foreignKeyName: "urdf_embeddings_id_fkey"
94
- columns: ["id"]
95
- isOneToOne: true
96
- referencedRelation: "urdf"
97
- referencedColumns: ["id"]
98
- },
99
- ]
100
- }
101
- }
102
- Views: {
103
- [_ in never]: never
104
- }
105
- Functions: {
106
- binary_quantize: {
107
- Args: { "": string } | { "": unknown }
108
- Returns: unknown
109
- }
110
- halfvec_avg: {
111
- Args: { "": number[] }
112
- Returns: unknown
113
- }
114
- halfvec_out: {
115
- Args: { "": unknown }
116
- Returns: unknown
117
- }
118
- halfvec_send: {
119
- Args: { "": unknown }
120
- Returns: string
121
- }
122
- halfvec_typmod_in: {
123
- Args: { "": unknown[] }
124
- Returns: number
125
- }
126
- hnsw_bit_support: {
127
- Args: { "": unknown }
128
- Returns: unknown
129
- }
130
- hnsw_halfvec_support: {
131
- Args: { "": unknown }
132
- Returns: unknown
133
- }
134
- hnsw_sparsevec_support: {
135
- Args: { "": unknown }
136
- Returns: unknown
137
- }
138
- hnswhandler: {
139
- Args: { "": unknown }
140
- Returns: unknown
141
- }
142
- ivfflat_bit_support: {
143
- Args: { "": unknown }
144
- Returns: unknown
145
- }
146
- ivfflat_halfvec_support: {
147
- Args: { "": unknown }
148
- Returns: unknown
149
- }
150
- ivfflathandler: {
151
- Args: { "": unknown }
152
- Returns: unknown
153
- }
154
- l2_norm: {
155
- Args: { "": unknown } | { "": unknown }
156
- Returns: number
157
- }
158
- l2_normalize: {
159
- Args: { "": string } | { "": unknown } | { "": unknown }
160
- Returns: string
161
- }
162
- match_urdfs: {
163
- Args: {
164
- query_embedding: string
165
- match_threshold: number
166
- match_count: number
167
- }
168
- Returns: {
169
- id: string
170
- name: string
171
- summary: string
172
- score: number
173
- }[]
174
- }
175
- sparsevec_out: {
176
- Args: { "": unknown }
177
- Returns: unknown
178
- }
179
- sparsevec_send: {
180
- Args: { "": unknown }
181
- Returns: string
182
- }
183
- sparsevec_typmod_in: {
184
- Args: { "": unknown[] }
185
- Returns: number
186
- }
187
- vector_avg: {
188
- Args: { "": number[] }
189
- Returns: string
190
- }
191
- vector_dims: {
192
- Args: { "": string } | { "": unknown }
193
- Returns: number
194
- }
195
- vector_norm: {
196
- Args: { "": string }
197
- Returns: number
198
- }
199
- vector_out: {
200
- Args: { "": string }
201
- Returns: unknown
202
- }
203
- vector_send: {
204
- Args: { "": string }
205
- Returns: string
206
- }
207
- vector_typmod_in: {
208
- Args: { "": unknown[] }
209
- Returns: number
210
- }
211
- }
212
- Enums: {
213
- [_ in never]: never
214
- }
215
- CompositeTypes: {
216
- [_ in never]: never
217
- }
218
- }
219
- }
220
-
221
- type DefaultSchema = Database[Extract<keyof Database, "public">]
222
-
223
- export type Tables<
224
- DefaultSchemaTableNameOrOptions extends
225
- | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
226
- | { schema: keyof Database },
227
- TableName extends DefaultSchemaTableNameOrOptions extends {
228
- schema: keyof Database
229
- }
230
- ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
231
- Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
232
- : never = never,
233
- > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
234
- ? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
235
- Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
236
- Row: infer R
237
- }
238
- ? R
239
- : never
240
- : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
241
- DefaultSchema["Views"])
242
- ? (DefaultSchema["Tables"] &
243
- DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
244
- Row: infer R
245
- }
246
- ? R
247
- : never
248
- : never
249
-
250
- export type TablesInsert<
251
- DefaultSchemaTableNameOrOptions extends
252
- | keyof DefaultSchema["Tables"]
253
- | { schema: keyof Database },
254
- TableName extends DefaultSchemaTableNameOrOptions extends {
255
- schema: keyof Database
256
- }
257
- ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
258
- : never = never,
259
- > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
260
- ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
261
- Insert: infer I
262
- }
263
- ? I
264
- : never
265
- : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
266
- ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
267
- Insert: infer I
268
- }
269
- ? I
270
- : never
271
- : never
272
-
273
- export type TablesUpdate<
274
- DefaultSchemaTableNameOrOptions extends
275
- | keyof DefaultSchema["Tables"]
276
- | { schema: keyof Database },
277
- TableName extends DefaultSchemaTableNameOrOptions extends {
278
- schema: keyof Database
279
- }
280
- ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
281
- : never = never,
282
- > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
283
- ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
284
- Update: infer U
285
- }
286
- ? U
287
- : never
288
- : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
289
- ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
290
- Update: infer U
291
- }
292
- ? U
293
- : never
294
- : never
295
-
296
- export type Enums<
297
- DefaultSchemaEnumNameOrOptions extends
298
- | keyof DefaultSchema["Enums"]
299
- | { schema: keyof Database },
300
- EnumName extends DefaultSchemaEnumNameOrOptions extends {
301
- schema: keyof Database
302
- }
303
- ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
304
- : never = never,
305
- > = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
306
- ? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
307
- : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
308
- ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
309
- : never
310
-
311
- export type CompositeTypes<
312
- PublicCompositeTypeNameOrOptions extends
313
- | keyof DefaultSchema["CompositeTypes"]
314
- | { schema: keyof Database },
315
- CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
316
- schema: keyof Database
317
- }
318
- ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
319
- : never = never,
320
- > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
321
- ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
322
- : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
323
- ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
324
- : never
325
-
326
- export const Constants = {
327
- public: {
328
- Enums: {},
329
- },
330
- } as const
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
viewer/supabase/.temp/cli-latest DELETED
@@ -1 +0,0 @@
1
- v2.20.12
 
 
viewer/supabase/functions/create-animation/index.ts DELETED
@@ -1,517 +0,0 @@
1
- // @deno-types="npm:@types/node"
2
- import { XMLParser } from "npm:[email protected]";
3
- import { serve } from "https://deno.land/[email protected]/http/server.ts";
4
- import { createClient } from "https://esm.sh/@supabase/[email protected]";
5
-
6
- // Deno environment
7
- declare const Deno: {
8
- env: {
9
- get(key: string): string | undefined;
10
- };
11
- };
12
-
13
- // OpenAI client
14
- const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
15
- if (!OPENAI_API_KEY) {
16
- console.error("Missing OPENAI_API_KEY environment variable");
17
- }
18
-
19
- // URDF Joint type definition
20
- interface URDFJoint {
21
- name: string;
22
- type: string;
23
- parent: string;
24
- child: string;
25
- axis?: {
26
- xyz: string;
27
- };
28
- origin?: {
29
- xyz: string;
30
- rpy: string;
31
- };
32
- limits?: {
33
- lower?: number;
34
- upper?: number;
35
- effort?: number;
36
- velocity?: number;
37
- };
38
- }
39
-
40
- // URDF Element interface to avoid 'any' type
41
- interface URDFElement {
42
- name?: string;
43
- type?: string;
44
- parent?: { link?: string };
45
- child?: { link?: string };
46
- axis?: { xyz?: string };
47
- origin?: { xyz?: string; rpy?: string };
48
- limit?: {
49
- lower?: number;
50
- upper?: number;
51
- effort?: number;
52
- velocity?: number;
53
- };
54
- }
55
-
56
- // Animation request definition
57
- interface AnimationRequest {
58
- robotName: string;
59
- urdfContent: string;
60
- description: string;
61
- }
62
-
63
- // Joint animation configuration
64
- interface JointAnimationConfig {
65
- name: string;
66
- type: "sine" | "linear" | "constant";
67
- min: number;
68
- max: number;
69
- speed: number;
70
- offset: number;
71
- isDegrees?: boolean;
72
- }
73
-
74
- // Robot animation configuration
75
- interface RobotAnimationConfig {
76
- joints: JointAnimationConfig[];
77
- speedMultiplier?: number;
78
- }
79
-
80
- // CORS headers for cross-origin requests
81
- const corsHeaders = {
82
- "Access-Control-Allow-Origin": "*",
83
- "Access-Control-Allow-Headers":
84
- "authorization, x-client-info, apikey, content-type",
85
- "Access-Control-Allow-Methods": "POST, OPTIONS",
86
- };
87
-
88
- // Generate animation configuration using OpenAI
89
- async function generateAnimationConfig(
90
- joints: URDFJoint[],
91
- description: string,
92
- robotName: string
93
- ): Promise<RobotAnimationConfig> {
94
- // Filter out fixed joints as they can't be animated
95
- const movableJoints = joints.filter(
96
- (joint) =>
97
- joint.type !== "fixed" &&
98
- joint.name &&
99
- (joint.type === "revolute" ||
100
- joint.type === "continuous" ||
101
- joint.type === "prismatic")
102
- );
103
-
104
- // Create a description of available joints for the AI
105
- const jointsDescription = movableJoints
106
- .map((joint) => {
107
- const limits = joint.limits
108
- ? `(limits: ${
109
- joint.limits.lower !== undefined ? joint.limits.lower : "none"
110
- } to ${
111
- joint.limits.upper !== undefined ? joint.limits.upper : "none"
112
- })`
113
- : "(no limits)";
114
-
115
- const axisInfo = joint.axis ? `axis: ${joint.axis.xyz}` : "no axis info";
116
-
117
- return `- ${joint.name}: type=${joint.type}, connects ${joint.parent} to ${joint.child}, ${axisInfo} ${limits}`;
118
- })
119
- .join("\n");
120
-
121
- // Create prompt for OpenAI
122
- const prompt = `
123
- You are a robotics animation expert. I need you to create animation configurations for a robot named "${robotName}".
124
-
125
- The user wants the following animation: "${description}"
126
-
127
- Here are the available movable joints in the robot:
128
- ${jointsDescription}
129
-
130
- Based on these joints and the user's description, create a valid animation configuration in JSON format.
131
- The animation configuration should include:
132
- 1. A list of joint animations (only include joints that should move)
133
- 2. For each joint, specify:
134
- - name: The exact joint name from the list above (string)
135
- - type: Must be exactly one of "sine", "linear", or "constant" (string)
136
- - min: Minimum position value (number, respect joint limits if available)
137
- - max: Maximum position value (number, respect joint limits if available)
138
- - speed: Speed multiplier (number, 0.5-2.0 is a reasonable range, lower is slower)
139
- - offset: Phase offset in radians (number, 0 to 2π, useful for coordinating multiple joints)
140
- - isDegrees: Optional boolean, set to true if the min/max values are in degrees rather than radians
141
-
142
- 3. An optional global speedMultiplier (number, default: 1.0)
143
-
144
- You must return ONLY valid JSON matching this EXACT schema:
145
- {
146
- "joints": [
147
- {
148
- "name": string,
149
- "type": "sine" | "linear" | "constant",
150
- "min": number,
151
- "max": number,
152
- "speed": number,
153
- "offset": number,
154
- "isDegrees"?: boolean
155
- }
156
- ],
157
- "speedMultiplier"?: number
158
- }
159
-
160
- Important rules:
161
- - Only include movable joints from the list provided
162
- - Respect joint limits when available
163
- - Create natural, coordinated movements that match the user's description
164
- - If the description mentions specific joints, prioritize animating those
165
- - For walking animations, coordinate leg joints with appropriate phase offsets
166
- - For realistic motion, use sine waves with different offsets for natural movement
167
- - DO NOT ADD ANY PROPERTIES that aren't in the schema above
168
- - DO NOT INCLUDE customEasing or any other properties not in the schema
169
- - Return ONLY valid JSON without comments or explanations
170
- `;
171
-
172
- try {
173
- // Call OpenAI API
174
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
175
- method: "POST",
176
- headers: {
177
- "Content-Type": "application/json",
178
- Authorization: `Bearer ${OPENAI_API_KEY}`,
179
- },
180
- body: JSON.stringify({
181
- model: "gpt-4o-mini",
182
- messages: [
183
- {
184
- role: "system",
185
- content:
186
- "You are a robotics animation expert that produces only valid JSON as output.",
187
- },
188
- {
189
- role: "user",
190
- content: prompt,
191
- },
192
- ],
193
- temperature: 0.7,
194
- max_tokens: 1000,
195
- response_format: { type: "json_object" },
196
- }),
197
- });
198
-
199
- if (!response.ok) {
200
- const errorData = await response.json();
201
- console.error("OpenAI API error:", errorData);
202
- throw new Error(`OpenAI API error: ${response.status}`);
203
- }
204
-
205
- const data = await response.json();
206
- const animationText = data.choices[0].message.content.trim();
207
-
208
- // Log the raw response for debugging
209
- console.log(
210
- "Raw OpenAI response:",
211
- animationText.substring(0, 200) +
212
- (animationText.length > 200 ? "..." : "")
213
- );
214
-
215
- // Parse JSON from the response
216
- let animationConfig: RobotAnimationConfig;
217
- try {
218
- // Since we're using response_format: { type: "json_object" }, this should always be valid JSON
219
- animationConfig = JSON.parse(animationText);
220
- } catch (e) {
221
- console.error("Failed to parse JSON response:", e);
222
- throw new Error(
223
- "Could not parse animation configuration from AI response"
224
- );
225
- }
226
-
227
- // Validate the animation config against our RobotAnimationConfig type
228
- if (!animationConfig.joints || !Array.isArray(animationConfig.joints)) {
229
- throw new Error(
230
- "Invalid animation configuration: missing or invalid joints array"
231
- );
232
- }
233
-
234
- // Default speedMultiplier if not provided
235
- if (animationConfig.speedMultiplier === undefined) {
236
- animationConfig.speedMultiplier = 1.0;
237
- } else if (typeof animationConfig.speedMultiplier !== "number") {
238
- throw new Error("Invalid speedMultiplier: must be a number");
239
- }
240
-
241
- // Validate all joints have required properties and correct types
242
- for (const joint of animationConfig.joints) {
243
- // Check required properties
244
- if (!joint.name || typeof joint.name !== "string") {
245
- throw new Error("Invalid joint: name is required and must be a string");
246
- }
247
-
248
- if (!joint.type || !["sine", "linear", "constant"].includes(joint.type)) {
249
- throw new Error(
250
- `Invalid joint type: ${joint.type} for joint ${joint.name}. Must be "sine", "linear", or "constant"`
251
- );
252
- }
253
-
254
- if (joint.min === undefined || typeof joint.min !== "number") {
255
- throw new Error(
256
- `Invalid min value for joint ${joint.name}: must be a number`
257
- );
258
- }
259
-
260
- if (joint.max === undefined || typeof joint.max !== "number") {
261
- throw new Error(
262
- `Invalid max value for joint ${joint.name}: must be a number`
263
- );
264
- }
265
-
266
- if (joint.speed === undefined || typeof joint.speed !== "number") {
267
- throw new Error(
268
- `Invalid speed value for joint ${joint.name}: must be a number`
269
- );
270
- }
271
-
272
- if (joint.offset === undefined || typeof joint.offset !== "number") {
273
- throw new Error(
274
- `Invalid offset value for joint ${joint.name}: must be a number`
275
- );
276
- }
277
-
278
- // Check optional properties
279
- if (
280
- joint.isDegrees !== undefined &&
281
- typeof joint.isDegrees !== "boolean"
282
- ) {
283
- throw new Error(
284
- `Invalid isDegrees value for joint ${joint.name}: must be a boolean`
285
- );
286
- }
287
-
288
- // Ensure no custom properties that aren't in our type
289
- const allowedProperties = [
290
- "name",
291
- "type",
292
- "min",
293
- "max",
294
- "speed",
295
- "offset",
296
- "isDegrees",
297
- ];
298
- const extraProperties = Object.keys(joint).filter(
299
- (key) => !allowedProperties.includes(key)
300
- );
301
-
302
- if (extraProperties.length > 0) {
303
- console.warn(
304
- `Warning: Joint ${
305
- joint.name
306
- } has extra properties not in JointAnimationConfig: ${extraProperties.join(
307
- ", "
308
- )}`
309
- );
310
- // Remove extra properties to ensure exact type match
311
- extraProperties.forEach((prop) => {
312
- delete joint[prop];
313
- });
314
- }
315
- }
316
-
317
- // Clean the final object to ensure it matches our type exactly
318
- const cleanedConfig: RobotAnimationConfig = {
319
- joints: animationConfig.joints.map((joint) => ({
320
- name: joint.name,
321
- type: joint.type,
322
- min: joint.min,
323
- max: joint.max,
324
- speed: joint.speed,
325
- offset: joint.offset,
326
- ...(joint.isDegrees !== undefined && { isDegrees: joint.isDegrees }),
327
- })),
328
- speedMultiplier: animationConfig.speedMultiplier,
329
- };
330
-
331
- // Log the cleaned config for debugging
332
- console.log(
333
- `Cleaned config: ${cleanedConfig.joints.length} joints, speedMultiplier: ${cleanedConfig.speedMultiplier}`
334
- );
335
-
336
- return cleanedConfig;
337
- } catch (error) {
338
- console.error("Error generating animation:", error);
339
- // Return a simple default animation if generation fails
340
-
341
- // Get at most 2 movable joints to animate
342
- const jointsToAnimate = movableJoints.slice(0, 2);
343
-
344
- if (jointsToAnimate.length === 0) {
345
- // If no movable joints found, create a message
346
- throw new Error(
347
- "Cannot generate animation: No movable joints found in the robot model"
348
- );
349
- }
350
-
351
- // Create a simple, safe animation for the available joints
352
- const fallbackConfig: RobotAnimationConfig = {
353
- joints: jointsToAnimate.map((joint) => ({
354
- name: joint.name,
355
- type: "sine" as const,
356
- min: joint.limits?.lower !== undefined ? joint.limits.lower : -0.5,
357
- max: joint.limits?.upper !== undefined ? joint.limits.upper : 0.5,
358
- speed: 1.0,
359
- offset: 0,
360
- isDegrees: false,
361
- })),
362
- speedMultiplier: 1.0,
363
- };
364
-
365
- console.log("Using fallback animation config:", fallbackConfig);
366
- return fallbackConfig;
367
- }
368
- }
369
-
370
- // Parse URDF XML and extract joint information
371
- function parseUrdfForJoints(urdfContent: string): URDFJoint[] {
372
- const parser = new XMLParser({
373
- ignoreAttributes: false,
374
- attributeNamePrefix: "",
375
- parseAttributeValue: true,
376
- });
377
-
378
- try {
379
- const doc = parser.parse(urdfContent);
380
-
381
- if (!doc || !doc.robot) {
382
- throw new Error("No robot element found in URDF");
383
- }
384
-
385
- // Extract joints
386
- const joints: URDFJoint[] = [];
387
- const robotElement = doc.robot;
388
-
389
- if (robotElement.joint) {
390
- const jointElements = Array.isArray(robotElement.joint)
391
- ? robotElement.joint
392
- : [robotElement.joint];
393
-
394
- jointElements.forEach((joint: URDFElement) => {
395
- const jointData: URDFJoint = {
396
- name: joint.name || "",
397
- type: joint.type || "",
398
- parent: joint.parent?.link || "",
399
- child: joint.child?.link || "",
400
- };
401
-
402
- // Parse axis
403
- if (joint.axis) {
404
- jointData.axis = {
405
- xyz: joint.axis.xyz || "0 0 0",
406
- };
407
- }
408
-
409
- // Parse origin
410
- if (joint.origin) {
411
- jointData.origin = {
412
- xyz: joint.origin.xyz || "0 0 0",
413
- rpy: joint.origin.rpy || "0 0 0",
414
- };
415
- }
416
-
417
- // Parse limits
418
- if (joint.limit) {
419
- jointData.limits = {
420
- lower:
421
- joint.limit.lower !== undefined ? joint.limit.lower : undefined,
422
- upper:
423
- joint.limit.upper !== undefined ? joint.limit.upper : undefined,
424
- effort:
425
- joint.limit.effort !== undefined ? joint.limit.effort : undefined,
426
- velocity:
427
- joint.limit.velocity !== undefined
428
- ? joint.limit.velocity
429
- : undefined,
430
- };
431
- }
432
-
433
- joints.push(jointData);
434
- });
435
- }
436
-
437
- return joints;
438
- } catch (error) {
439
- console.error("Error parsing URDF XML:", error);
440
- throw new Error(
441
- `Could not parse URDF: ${
442
- error instanceof Error ? error.message : String(error)
443
- }`
444
- );
445
- }
446
- }
447
-
448
- // Main server function
449
- serve(async (req) => {
450
- // Handle CORS preflight requests
451
- if (req.method === "OPTIONS") {
452
- return new Response("ok", { headers: corsHeaders });
453
- }
454
-
455
- try {
456
- if (req.method !== "POST") {
457
- throw new Error("Method not allowed");
458
- }
459
-
460
- // Parse request
461
- const requestData: AnimationRequest = await req.json();
462
- const { robotName, urdfContent, description } = requestData;
463
-
464
- if (!urdfContent) {
465
- throw new Error("No URDF content provided");
466
- }
467
-
468
- if (!description) {
469
- throw new Error("No animation description provided");
470
- }
471
-
472
- console.log(
473
- `Generating animation for ${robotName}: "${description.substring(
474
- 0,
475
- 100
476
- )}..."`
477
- );
478
-
479
- // Parse URDF to extract joint information
480
- const joints = parseUrdfForJoints(urdfContent);
481
- console.log(`Extracted ${joints.length} joints from URDF`);
482
-
483
- // Generate animation configuration
484
- const animationConfig = await generateAnimationConfig(
485
- joints,
486
- description,
487
- robotName
488
- );
489
- console.log(
490
- `Generated animation with ${animationConfig.joints.length} animated joints`
491
- );
492
-
493
- // Return the animation configuration
494
- return new Response(JSON.stringify(animationConfig), {
495
- headers: {
496
- ...corsHeaders,
497
- "Content-Type": "application/json",
498
- },
499
- });
500
- } catch (error) {
501
- console.error("Error processing animation request:", error);
502
-
503
- return new Response(
504
- JSON.stringify({
505
- error:
506
- error instanceof Error ? error.message : "Unknown error occurred",
507
- }),
508
- {
509
- status: 500,
510
- headers: {
511
- ...corsHeaders,
512
- "Content-Type": "application/json",
513
- },
514
- }
515
- );
516
- }
517
- });