Spaces:
Running
Running
refactor: get rid of unused stuff from old project
Browse files- viewer/src/api/urdfApi.ts +0 -103
- viewer/src/components/AnimationDialog.tsx +0 -139
- viewer/src/components/Carousel.tsx +0 -175
- viewer/src/components/ChatWindow.tsx +0 -309
- viewer/src/components/ContentCarousel.tsx +0 -29
- viewer/src/components/Header.tsx +0 -150
- viewer/src/components/TeamSection.tsx +0 -83
- viewer/src/hooks/use-my-list.tsx +0 -74
- viewer/src/hooks/useChatApi.ts +0 -129
- viewer/src/hooks/useCreateAnimation.ts +0 -114
- viewer/src/integrations/supabase/types.ts +0 -330
- viewer/supabase/.temp/cli-latest +0 -1
- viewer/supabase/functions/create-animation/index.ts +0 -517
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 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|