Spaces:
Running
Running
Sunhey Cho
commited on
Commit
·
9aee46b
1
Parent(s):
0e9cdf3
Add frontend source code structure for direct development
Browse files- frontend/package-lock.json +0 -0
- frontend/package.json +42 -0
- frontend/public/index.html +24 -0
- frontend/public/manifest.json +15 -0
- frontend/src/App.css +47 -0
- frontend/src/App.js +207 -0
- frontend/src/components/ImageUploader.js +193 -0
- frontend/src/components/LlmAnalysis.js +156 -0
- frontend/src/components/ModelSelector.js +168 -0
- frontend/src/components/OpenAIChat.js +136 -0
- frontend/src/components/ResultDisplay.js +198 -0
- frontend/src/components/VectorDBActions.js +434 -0
- frontend/src/index.css +14 -0
- frontend/src/index.js +17 -0
- frontend/src/reportWebVitals.js +13 -0
frontend/package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
frontend/package.json
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "vision-web-app",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"dependencies": {
|
6 |
+
"@material-ui/core": "^4.12.4",
|
7 |
+
"@material-ui/icons": "^4.11.3",
|
8 |
+
"@material-ui/lab": "^4.0.0-alpha.61",
|
9 |
+
"@testing-library/jest-dom": "^4.2.4",
|
10 |
+
"@testing-library/react": "^9.5.0",
|
11 |
+
"@testing-library/user-event": "^7.2.1",
|
12 |
+
"axios": "^0.21.1",
|
13 |
+
"react": "^16.14.0",
|
14 |
+
"react-dom": "^16.14.0",
|
15 |
+
"react-scripts": "3.4.3",
|
16 |
+
"web-vitals": "^0.2.4"
|
17 |
+
},
|
18 |
+
"scripts": {
|
19 |
+
"start": "react-scripts start",
|
20 |
+
"build": "react-scripts build",
|
21 |
+
"test": "react-scripts test",
|
22 |
+
"eject": "react-scripts eject"
|
23 |
+
},
|
24 |
+
"eslintConfig": {
|
25 |
+
"extends": [
|
26 |
+
"react-app"
|
27 |
+
]
|
28 |
+
},
|
29 |
+
"browserslist": {
|
30 |
+
"production": [
|
31 |
+
">0.2%",
|
32 |
+
"not dead",
|
33 |
+
"not op_mini all"
|
34 |
+
],
|
35 |
+
"development": [
|
36 |
+
"last 1 chrome version",
|
37 |
+
"last 1 firefox version",
|
38 |
+
"last 1 safari version"
|
39 |
+
]
|
40 |
+
},
|
41 |
+
"proxy": "http://localhost:5000"
|
42 |
+
}
|
frontend/public/index.html
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7 |
+
<meta name="theme-color" content="#000000" />
|
8 |
+
<meta
|
9 |
+
name="description"
|
10 |
+
content="Multi-Model Object Detection Demo"
|
11 |
+
/>
|
12 |
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
13 |
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
14 |
+
<title>Vision Web App</title>
|
15 |
+
<link
|
16 |
+
rel="stylesheet"
|
17 |
+
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
18 |
+
/>
|
19 |
+
</head>
|
20 |
+
<body>
|
21 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
22 |
+
<div id="root"></div>
|
23 |
+
</body>
|
24 |
+
</html>
|
frontend/public/manifest.json
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"short_name": "Vision Web App",
|
3 |
+
"name": "Multi-Model Object Detection Demo",
|
4 |
+
"icons": [
|
5 |
+
{
|
6 |
+
"src": "favicon.ico",
|
7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
8 |
+
"type": "image/x-icon"
|
9 |
+
}
|
10 |
+
],
|
11 |
+
"start_url": ".",
|
12 |
+
"display": "standalone",
|
13 |
+
"theme_color": "#000000",
|
14 |
+
"background_color": "#ffffff"
|
15 |
+
}
|
frontend/src/App.css
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.App {
|
2 |
+
text-align: center;
|
3 |
+
}
|
4 |
+
|
5 |
+
.preview-image {
|
6 |
+
max-width: 100%;
|
7 |
+
max-height: 300px;
|
8 |
+
margin-top: 16px;
|
9 |
+
}
|
10 |
+
|
11 |
+
.result-image {
|
12 |
+
max-width: 100%;
|
13 |
+
border: 1px solid #ddd;
|
14 |
+
border-radius: 4px;
|
15 |
+
padding: 4px;
|
16 |
+
}
|
17 |
+
|
18 |
+
.detection-list {
|
19 |
+
margin-top: 16px;
|
20 |
+
text-align: left;
|
21 |
+
}
|
22 |
+
|
23 |
+
.model-card {
|
24 |
+
cursor: pointer;
|
25 |
+
transition: all 0.3s;
|
26 |
+
}
|
27 |
+
|
28 |
+
.model-card:hover {
|
29 |
+
transform: translateY(-5px);
|
30 |
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
31 |
+
}
|
32 |
+
|
33 |
+
.model-card.selected {
|
34 |
+
border: 2px solid #3f51b5;
|
35 |
+
background-color: #e8eaf6;
|
36 |
+
}
|
37 |
+
|
38 |
+
.model-card.disabled {
|
39 |
+
opacity: 0.6;
|
40 |
+
cursor: not-allowed;
|
41 |
+
}
|
42 |
+
|
43 |
+
.performance-info {
|
44 |
+
margin-top: 16px;
|
45 |
+
font-size: 0.9rem;
|
46 |
+
color: #666;
|
47 |
+
}
|
frontend/src/App.js
ADDED
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from 'react';
|
2 |
+
import {
|
3 |
+
Container,
|
4 |
+
Typography,
|
5 |
+
Box,
|
6 |
+
Paper,
|
7 |
+
Grid,
|
8 |
+
CircularProgress,
|
9 |
+
AppBar,
|
10 |
+
Toolbar,
|
11 |
+
ThemeProvider,
|
12 |
+
createMuiTheme
|
13 |
+
} from '@material-ui/core';
|
14 |
+
import ImageUploader from './components/ImageUploader';
|
15 |
+
import ModelSelector from './components/ModelSelector';
|
16 |
+
import ResultDisplay from './components/ResultDisplay';
|
17 |
+
import LlmAnalysis from './components/LlmAnalysis';
|
18 |
+
import OpenAIChat from './components/OpenAIChat';
|
19 |
+
import './App.css';
|
20 |
+
|
21 |
+
// Create a theme
|
22 |
+
const theme = createMuiTheme({
|
23 |
+
palette: {
|
24 |
+
primary: {
|
25 |
+
main: '#3f51b5',
|
26 |
+
},
|
27 |
+
secondary: {
|
28 |
+
main: '#f50057',
|
29 |
+
},
|
30 |
+
},
|
31 |
+
typography: {
|
32 |
+
fontFamily: 'Roboto, Arial, sans-serif',
|
33 |
+
},
|
34 |
+
});
|
35 |
+
|
36 |
+
function App() {
|
37 |
+
const [selectedImage, setSelectedImage] = useState(null);
|
38 |
+
const [selectedModel, setSelectedModel] = useState('');
|
39 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
40 |
+
const [results, setResults] = useState(null);
|
41 |
+
const [error, setError] = useState(null);
|
42 |
+
const [modelsStatus, setModelsStatus] = useState({
|
43 |
+
yolo: false,
|
44 |
+
detr: false,
|
45 |
+
vit: false
|
46 |
+
});
|
47 |
+
|
48 |
+
// Check API status on component mount
|
49 |
+
useEffect(() => {
|
50 |
+
fetch('/api/status')
|
51 |
+
.then(response => response.json())
|
52 |
+
.then(data => {
|
53 |
+
setModelsStatus(data.models);
|
54 |
+
})
|
55 |
+
.catch(err => {
|
56 |
+
console.error('Error checking API status:', err);
|
57 |
+
setError('Error connecting to the backend API. Please make sure the server is running.');
|
58 |
+
});
|
59 |
+
}, []);
|
60 |
+
|
61 |
+
const handleImageUpload = (image) => {
|
62 |
+
setSelectedImage(image);
|
63 |
+
setResults(null);
|
64 |
+
setError(null);
|
65 |
+
};
|
66 |
+
|
67 |
+
const handleModelSelect = (model) => {
|
68 |
+
setSelectedModel(model);
|
69 |
+
setResults(null);
|
70 |
+
setError(null);
|
71 |
+
};
|
72 |
+
|
73 |
+
const processImage = async () => {
|
74 |
+
if (!selectedImage || !selectedModel) {
|
75 |
+
setError('Please select both an image and a model');
|
76 |
+
return;
|
77 |
+
}
|
78 |
+
|
79 |
+
setIsProcessing(true);
|
80 |
+
setError(null);
|
81 |
+
|
82 |
+
// Create form data for the image
|
83 |
+
const formData = new FormData();
|
84 |
+
formData.append('image', selectedImage);
|
85 |
+
|
86 |
+
let endpoint = '';
|
87 |
+
switch (selectedModel) {
|
88 |
+
case 'yolo':
|
89 |
+
endpoint = '/api/detect/yolo';
|
90 |
+
break;
|
91 |
+
case 'detr':
|
92 |
+
endpoint = '/api/detect/detr';
|
93 |
+
break;
|
94 |
+
case 'vit':
|
95 |
+
endpoint = '/api/classify/vit';
|
96 |
+
break;
|
97 |
+
default:
|
98 |
+
setError('Invalid model selection');
|
99 |
+
setIsProcessing(false);
|
100 |
+
return;
|
101 |
+
}
|
102 |
+
|
103 |
+
try {
|
104 |
+
const response = await fetch(endpoint, {
|
105 |
+
method: 'POST',
|
106 |
+
body: formData,
|
107 |
+
});
|
108 |
+
|
109 |
+
if (!response.ok) {
|
110 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
111 |
+
}
|
112 |
+
|
113 |
+
const data = await response.json();
|
114 |
+
setResults({ model: selectedModel, data });
|
115 |
+
} catch (err) {
|
116 |
+
console.error('Error processing image:', err);
|
117 |
+
setError(`Error processing image: ${err.message}`);
|
118 |
+
} finally {
|
119 |
+
setIsProcessing(false);
|
120 |
+
}
|
121 |
+
};
|
122 |
+
|
123 |
+
return (
|
124 |
+
<ThemeProvider theme={theme}>
|
125 |
+
<Box style={{ flexGrow: 1 }}>
|
126 |
+
<AppBar position="static">
|
127 |
+
<Toolbar>
|
128 |
+
<Typography variant="h6" style={{ flexGrow: 1 }}>
|
129 |
+
Multi-Model Object Detection Demo
|
130 |
+
</Typography>
|
131 |
+
</Toolbar>
|
132 |
+
</AppBar>
|
133 |
+
<Container maxWidth="lg" style={{ marginTop: theme.spacing(4), marginBottom: theme.spacing(4) }}>
|
134 |
+
<Grid container spacing={3}>
|
135 |
+
<Grid item xs={12}>
|
136 |
+
<Paper style={{ padding: theme.spacing(2) }}>
|
137 |
+
<Typography variant="h5" gutterBottom>
|
138 |
+
Upload an image to see how each model performs!
|
139 |
+
</Typography>
|
140 |
+
<Typography variant="body1" paragraph>
|
141 |
+
This demo showcases three different object detection and image classification models:
|
142 |
+
</Typography>
|
143 |
+
<Typography variant="body1" component="div">
|
144 |
+
<ul>
|
145 |
+
<li><strong>YOLOv8</strong>: Fast and accurate object detection</li>
|
146 |
+
<li><strong>DETR</strong>: DEtection TRansformer for object detection</li>
|
147 |
+
<li><strong>ViT</strong>: Vision Transformer for image classification</li>
|
148 |
+
</ul>
|
149 |
+
</Typography>
|
150 |
+
</Paper>
|
151 |
+
</Grid>
|
152 |
+
|
153 |
+
<Grid item xs={12} md={6}>
|
154 |
+
<ImageUploader onImageUpload={handleImageUpload} />
|
155 |
+
</Grid>
|
156 |
+
|
157 |
+
<Grid item xs={12} md={6}>
|
158 |
+
<ModelSelector
|
159 |
+
onModelSelect={handleModelSelect}
|
160 |
+
onProcess={processImage}
|
161 |
+
isProcessing={isProcessing}
|
162 |
+
modelsStatus={modelsStatus}
|
163 |
+
selectedModel={selectedModel}
|
164 |
+
imageSelected={!!selectedImage}
|
165 |
+
/>
|
166 |
+
</Grid>
|
167 |
+
|
168 |
+
{error && (
|
169 |
+
<Grid item xs={12}>
|
170 |
+
<Paper style={{ padding: theme.spacing(2), backgroundColor: '#ffebee' }}>
|
171 |
+
<Typography color="error">{error}</Typography>
|
172 |
+
</Paper>
|
173 |
+
</Grid>
|
174 |
+
)}
|
175 |
+
|
176 |
+
{isProcessing && (
|
177 |
+
<Grid item xs={12} style={{ textAlign: 'center', margin: `${theme.spacing(4)}px 0` }}>
|
178 |
+
<CircularProgress />
|
179 |
+
<Typography variant="h6" style={{ marginTop: theme.spacing(2) }}>
|
180 |
+
Processing image...
|
181 |
+
</Typography>
|
182 |
+
</Grid>
|
183 |
+
)}
|
184 |
+
|
185 |
+
{results && (
|
186 |
+
<>
|
187 |
+
<Grid item xs={12}>
|
188 |
+
<ResultDisplay results={results} />
|
189 |
+
</Grid>
|
190 |
+
<Grid item xs={12}>
|
191 |
+
<LlmAnalysis visionResults={results.data} model={results.model} />
|
192 |
+
</Grid>
|
193 |
+
</>
|
194 |
+
)}
|
195 |
+
|
196 |
+
{/* OpenAI Chat section at the end */}
|
197 |
+
<Grid item xs={12}>
|
198 |
+
<OpenAIChat />
|
199 |
+
</Grid>
|
200 |
+
</Grid>
|
201 |
+
</Container>
|
202 |
+
</Box>
|
203 |
+
</ThemeProvider>
|
204 |
+
);
|
205 |
+
}
|
206 |
+
|
207 |
+
export default App;
|
frontend/src/components/ImageUploader.js
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useRef } from 'react';
|
2 |
+
import {
|
3 |
+
Paper,
|
4 |
+
Typography,
|
5 |
+
Box,
|
6 |
+
Button,
|
7 |
+
IconButton
|
8 |
+
} from '@material-ui/core';
|
9 |
+
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
10 |
+
import DeleteIcon from '@material-ui/icons/Delete';
|
11 |
+
import { makeStyles } from '@material-ui/core/styles';
|
12 |
+
|
13 |
+
const useStyles = makeStyles((theme) => ({
|
14 |
+
paper: {
|
15 |
+
padding: theme.spacing(2),
|
16 |
+
display: 'flex',
|
17 |
+
flexDirection: 'column',
|
18 |
+
alignItems: 'center',
|
19 |
+
height: '100%',
|
20 |
+
minHeight: 300,
|
21 |
+
transition: 'all 0.3s ease'
|
22 |
+
},
|
23 |
+
dragActive: {
|
24 |
+
border: '2px dashed #3f51b5',
|
25 |
+
backgroundColor: 'rgba(63, 81, 181, 0.05)'
|
26 |
+
},
|
27 |
+
dragInactive: {
|
28 |
+
border: '2px dashed #ccc',
|
29 |
+
backgroundColor: 'white'
|
30 |
+
},
|
31 |
+
uploadBox: {
|
32 |
+
display: 'flex',
|
33 |
+
flexDirection: 'column',
|
34 |
+
alignItems: 'center',
|
35 |
+
justifyContent: 'center',
|
36 |
+
height: '100%',
|
37 |
+
width: '100%',
|
38 |
+
cursor: 'pointer'
|
39 |
+
},
|
40 |
+
uploadIcon: {
|
41 |
+
fontSize: 60,
|
42 |
+
color: '#3f51b5',
|
43 |
+
marginBottom: theme.spacing(2)
|
44 |
+
},
|
45 |
+
supportText: {
|
46 |
+
marginTop: theme.spacing(2)
|
47 |
+
},
|
48 |
+
previewBox: {
|
49 |
+
display: 'flex',
|
50 |
+
flexDirection: 'column',
|
51 |
+
alignItems: 'center',
|
52 |
+
width: '100%',
|
53 |
+
height: '100%',
|
54 |
+
position: 'relative'
|
55 |
+
},
|
56 |
+
imageContainer: {
|
57 |
+
position: 'relative',
|
58 |
+
width: '100%',
|
59 |
+
height: '100%',
|
60 |
+
display: 'flex',
|
61 |
+
justifyContent: 'center',
|
62 |
+
alignItems: 'center',
|
63 |
+
overflow: 'hidden',
|
64 |
+
marginTop: theme.spacing(2)
|
65 |
+
},
|
66 |
+
deleteButton: {
|
67 |
+
position: 'absolute',
|
68 |
+
top: 0,
|
69 |
+
right: 0,
|
70 |
+
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
71 |
+
'&:hover': {
|
72 |
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}));
|
76 |
+
|
77 |
+
const ImageUploader = ({ onImageUpload }) => {
|
78 |
+
const classes = useStyles();
|
79 |
+
const [previewUrl, setPreviewUrl] = useState(null);
|
80 |
+
const [dragActive, setDragActive] = useState(false);
|
81 |
+
const fileInputRef = useRef(null);
|
82 |
+
|
83 |
+
const handleDrag = (e) => {
|
84 |
+
e.preventDefault();
|
85 |
+
e.stopPropagation();
|
86 |
+
if (e.type === 'dragenter' || e.type === 'dragover') {
|
87 |
+
setDragActive(true);
|
88 |
+
} else if (e.type === 'dragleave') {
|
89 |
+
setDragActive(false);
|
90 |
+
}
|
91 |
+
};
|
92 |
+
|
93 |
+
const handleDrop = (e) => {
|
94 |
+
e.preventDefault();
|
95 |
+
e.stopPropagation();
|
96 |
+
setDragActive(false);
|
97 |
+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
98 |
+
handleFiles(e.dataTransfer.files[0]);
|
99 |
+
}
|
100 |
+
};
|
101 |
+
|
102 |
+
const handleChange = (e) => {
|
103 |
+
e.preventDefault();
|
104 |
+
if (e.target.files && e.target.files[0]) {
|
105 |
+
handleFiles(e.target.files[0]);
|
106 |
+
}
|
107 |
+
};
|
108 |
+
|
109 |
+
const handleFiles = (file) => {
|
110 |
+
if (file.type.startsWith('image/')) {
|
111 |
+
setPreviewUrl(URL.createObjectURL(file));
|
112 |
+
onImageUpload(file);
|
113 |
+
} else {
|
114 |
+
alert('Please upload an image file');
|
115 |
+
}
|
116 |
+
};
|
117 |
+
|
118 |
+
const onButtonClick = () => {
|
119 |
+
fileInputRef.current.click();
|
120 |
+
};
|
121 |
+
|
122 |
+
const handleRemoveImage = () => {
|
123 |
+
setPreviewUrl(null);
|
124 |
+
onImageUpload(null);
|
125 |
+
fileInputRef.current.value = "";
|
126 |
+
};
|
127 |
+
|
128 |
+
return (
|
129 |
+
<Paper
|
130 |
+
className={`${classes.paper} ${dragActive ? classes.dragActive : classes.dragInactive}`}
|
131 |
+
onDragEnter={handleDrag}
|
132 |
+
onDragLeave={handleDrag}
|
133 |
+
onDragOver={handleDrag}
|
134 |
+
onDrop={handleDrop}
|
135 |
+
>
|
136 |
+
<input
|
137 |
+
ref={fileInputRef}
|
138 |
+
type="file"
|
139 |
+
accept="image/*"
|
140 |
+
onChange={handleChange}
|
141 |
+
style={{ display: 'none' }}
|
142 |
+
/>
|
143 |
+
|
144 |
+
{!previewUrl ? (
|
145 |
+
<Box
|
146 |
+
className={classes.uploadBox}
|
147 |
+
onClick={onButtonClick}
|
148 |
+
>
|
149 |
+
<CloudUploadIcon className={classes.uploadIcon} />
|
150 |
+
<Typography variant="h6" gutterBottom>
|
151 |
+
Drag & Drop an image here
|
152 |
+
</Typography>
|
153 |
+
<Typography variant="body2" color="textSecondary" gutterBottom>
|
154 |
+
or
|
155 |
+
</Typography>
|
156 |
+
<Button
|
157 |
+
variant="contained"
|
158 |
+
color="primary"
|
159 |
+
component="span"
|
160 |
+
startIcon={<CloudUploadIcon />}
|
161 |
+
>
|
162 |
+
Browse Files
|
163 |
+
</Button>
|
164 |
+
<Typography variant="body2" color="textSecondary" className={classes.supportText}>
|
165 |
+
Supported formats: JPG, PNG, GIF
|
166 |
+
</Typography>
|
167 |
+
</Box>
|
168 |
+
) : (
|
169 |
+
<Box className={classes.previewBox}>
|
170 |
+
<Typography variant="h6" gutterBottom>
|
171 |
+
Preview
|
172 |
+
</Typography>
|
173 |
+
<Box className={classes.imageContainer}>
|
174 |
+
<img
|
175 |
+
src={previewUrl}
|
176 |
+
alt="Preview"
|
177 |
+
className="preview-image"
|
178 |
+
/>
|
179 |
+
<IconButton
|
180 |
+
aria-label="delete"
|
181 |
+
className={classes.deleteButton}
|
182 |
+
onClick={handleRemoveImage}
|
183 |
+
>
|
184 |
+
<DeleteIcon />
|
185 |
+
</IconButton>
|
186 |
+
</Box>
|
187 |
+
</Box>
|
188 |
+
)}
|
189 |
+
</Paper>
|
190 |
+
);
|
191 |
+
};
|
192 |
+
|
193 |
+
export default ImageUploader;
|
frontend/src/components/LlmAnalysis.js
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import {
|
3 |
+
Paper,
|
4 |
+
Typography,
|
5 |
+
Box,
|
6 |
+
TextField,
|
7 |
+
Button,
|
8 |
+
CircularProgress,
|
9 |
+
Divider
|
10 |
+
} from '@material-ui/core';
|
11 |
+
import { makeStyles } from '@material-ui/core/styles';
|
12 |
+
|
13 |
+
const useStyles = makeStyles((theme) => ({
|
14 |
+
paper: {
|
15 |
+
padding: theme.spacing(2),
|
16 |
+
marginTop: theme.spacing(2)
|
17 |
+
},
|
18 |
+
marginBottom: {
|
19 |
+
marginBottom: theme.spacing(2)
|
20 |
+
},
|
21 |
+
dividerMargin: {
|
22 |
+
margin: `${theme.spacing(2)}px 0`
|
23 |
+
},
|
24 |
+
responseBox: {
|
25 |
+
padding: theme.spacing(2),
|
26 |
+
backgroundColor: '#f5f5f5',
|
27 |
+
borderRadius: theme.shape.borderRadius,
|
28 |
+
marginTop: theme.spacing(2),
|
29 |
+
whiteSpace: 'pre-wrap'
|
30 |
+
},
|
31 |
+
buttonProgress: {
|
32 |
+
marginLeft: theme.spacing(1)
|
33 |
+
}
|
34 |
+
}));
|
35 |
+
|
36 |
+
const LlmAnalysis = ({ visionResults, model }) => {
|
37 |
+
const classes = useStyles();
|
38 |
+
const [userQuery, setUserQuery] = useState('');
|
39 |
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
40 |
+
const [analysisResult, setAnalysisResult] = useState(null);
|
41 |
+
const [error, setError] = useState(null);
|
42 |
+
|
43 |
+
// Format time for display
|
44 |
+
const formatTime = (ms) => {
|
45 |
+
if (ms === undefined || ms === null || isNaN(ms)) return '-';
|
46 |
+
const num = Number(ms);
|
47 |
+
if (num < 1000) return `${num.toFixed(2)} ms`;
|
48 |
+
return `${(num / 1000).toFixed(2)} s`;
|
49 |
+
};
|
50 |
+
|
51 |
+
const handleAnalyze = async () => {
|
52 |
+
if (!userQuery.trim()) return;
|
53 |
+
|
54 |
+
setIsAnalyzing(true);
|
55 |
+
setError(null);
|
56 |
+
|
57 |
+
try {
|
58 |
+
const response = await fetch('/api/analyze', {
|
59 |
+
method: 'POST',
|
60 |
+
headers: {
|
61 |
+
'Content-Type': 'application/json',
|
62 |
+
},
|
63 |
+
body: JSON.stringify({
|
64 |
+
visionResults: visionResults,
|
65 |
+
userQuery: userQuery
|
66 |
+
}),
|
67 |
+
});
|
68 |
+
|
69 |
+
if (!response.ok) {
|
70 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
71 |
+
}
|
72 |
+
|
73 |
+
const data = await response.json();
|
74 |
+
|
75 |
+
if (data.error) {
|
76 |
+
setError(data.error);
|
77 |
+
} else {
|
78 |
+
setAnalysisResult(data);
|
79 |
+
}
|
80 |
+
} catch (err) {
|
81 |
+
console.error('Error analyzing with LLM:', err);
|
82 |
+
setError(`Error analyzing with LLM: ${err.message}`);
|
83 |
+
} finally {
|
84 |
+
setIsAnalyzing(false);
|
85 |
+
}
|
86 |
+
};
|
87 |
+
|
88 |
+
if (!visionResults) return null;
|
89 |
+
|
90 |
+
return (
|
91 |
+
<Paper className={classes.paper}>
|
92 |
+
<Typography variant="h6" gutterBottom>
|
93 |
+
Ask AI about the {model === 'vit' ? 'Classification' : 'Detection'} Results
|
94 |
+
</Typography>
|
95 |
+
|
96 |
+
<Typography variant="body2" className={classes.marginBottom}>
|
97 |
+
Ask a question about the detected objects or classifications to get an AI-powered analysis.
|
98 |
+
</Typography>
|
99 |
+
|
100 |
+
<TextField
|
101 |
+
fullWidth
|
102 |
+
label="Your question about the image"
|
103 |
+
variant="outlined"
|
104 |
+
value={userQuery}
|
105 |
+
onChange={(e) => setUserQuery(e.target.value)}
|
106 |
+
disabled={isAnalyzing}
|
107 |
+
className={classes.marginBottom}
|
108 |
+
placeholder={model === 'vit'
|
109 |
+
? "E.g., What category does this image belong to?"
|
110 |
+
: "E.g., How many people are in this image?"}
|
111 |
+
/>
|
112 |
+
|
113 |
+
<Button
|
114 |
+
variant="contained"
|
115 |
+
color="primary"
|
116 |
+
onClick={handleAnalyze}
|
117 |
+
disabled={isAnalyzing || !userQuery.trim()}
|
118 |
+
>
|
119 |
+
Analyze with AI
|
120 |
+
{isAnalyzing && <CircularProgress size={24} className={classes.buttonProgress} />}
|
121 |
+
</Button>
|
122 |
+
|
123 |
+
{error && (
|
124 |
+
<Box mt={2}>
|
125 |
+
<Typography color="error">{error}</Typography>
|
126 |
+
</Box>
|
127 |
+
)}
|
128 |
+
|
129 |
+
{analysisResult && (
|
130 |
+
<>
|
131 |
+
<Divider className={classes.dividerMargin} />
|
132 |
+
|
133 |
+
<Typography variant="subtitle1" gutterBottom>
|
134 |
+
AI Analysis:
|
135 |
+
</Typography>
|
136 |
+
|
137 |
+
<Box className={classes.responseBox}>
|
138 |
+
<Typography variant="body1">
|
139 |
+
{analysisResult.response}
|
140 |
+
</Typography>
|
141 |
+
</Box>
|
142 |
+
|
143 |
+
{analysisResult.performance && (
|
144 |
+
<Box mt={1}>
|
145 |
+
<Typography variant="body2" color="textSecondary">
|
146 |
+
Analysis time: {formatTime(analysisResult.performance.inference_time)} on {analysisResult.performance.device}
|
147 |
+
</Typography>
|
148 |
+
</Box>
|
149 |
+
)}
|
150 |
+
</>
|
151 |
+
)}
|
152 |
+
</Paper>
|
153 |
+
);
|
154 |
+
};
|
155 |
+
|
156 |
+
export default LlmAnalysis;
|
frontend/src/components/ModelSelector.js
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import {
|
3 |
+
Grid,
|
4 |
+
Card,
|
5 |
+
CardContent,
|
6 |
+
CardActions,
|
7 |
+
Typography,
|
8 |
+
Button,
|
9 |
+
Chip,
|
10 |
+
Box
|
11 |
+
} from '@material-ui/core';
|
12 |
+
import VisibilityIcon from '@material-ui/icons/Visibility';
|
13 |
+
import CategoryIcon from '@material-ui/icons/Category';
|
14 |
+
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
15 |
+
import { makeStyles } from '@material-ui/core/styles';
|
16 |
+
|
17 |
+
const useStyles = makeStyles((theme) => ({
|
18 |
+
card: {
|
19 |
+
height: '100%',
|
20 |
+
display: 'flex',
|
21 |
+
flexDirection: 'column',
|
22 |
+
},
|
23 |
+
selectedCard: {
|
24 |
+
border: '2px solid #3f51b5',
|
25 |
+
},
|
26 |
+
unavailableCard: {
|
27 |
+
opacity: 0.6,
|
28 |
+
},
|
29 |
+
cardContent: {
|
30 |
+
flexGrow: 1,
|
31 |
+
},
|
32 |
+
chipContainer: {
|
33 |
+
marginBottom: theme.spacing(1.5),
|
34 |
+
},
|
35 |
+
successChip: {
|
36 |
+
backgroundColor: '#34C759',
|
37 |
+
color: '#fff',
|
38 |
+
},
|
39 |
+
errorChip: {
|
40 |
+
backgroundColor: '#FF3B3F',
|
41 |
+
color: '#fff',
|
42 |
+
},
|
43 |
+
modelType: {
|
44 |
+
marginTop: theme.spacing(1),
|
45 |
+
},
|
46 |
+
processButton: {
|
47 |
+
marginTop: theme.spacing(3),
|
48 |
+
textAlign: 'center',
|
49 |
+
}
|
50 |
+
}));
|
51 |
+
|
52 |
+
const ModelSelector = ({
|
53 |
+
onModelSelect,
|
54 |
+
onProcess,
|
55 |
+
isProcessing,
|
56 |
+
modelsStatus,
|
57 |
+
selectedModel,
|
58 |
+
imageSelected
|
59 |
+
}) => {
|
60 |
+
const classes = useStyles();
|
61 |
+
|
62 |
+
const models = [
|
63 |
+
{
|
64 |
+
id: 'yolo',
|
65 |
+
name: 'YOLOv8',
|
66 |
+
description: 'Fast and accurate object detection',
|
67 |
+
icon: <VisibilityIcon />,
|
68 |
+
available: modelsStatus.yolo
|
69 |
+
},
|
70 |
+
{
|
71 |
+
id: 'detr',
|
72 |
+
name: 'DETR',
|
73 |
+
description: 'DEtection TRansformer for object detection',
|
74 |
+
icon: <VisibilityIcon />,
|
75 |
+
available: modelsStatus.detr
|
76 |
+
},
|
77 |
+
{
|
78 |
+
id: 'vit',
|
79 |
+
name: 'ViT',
|
80 |
+
description: 'Vision Transformer for image classification',
|
81 |
+
icon: <CategoryIcon />,
|
82 |
+
available: modelsStatus.vit
|
83 |
+
}
|
84 |
+
];
|
85 |
+
|
86 |
+
const handleModelClick = (modelId) => {
|
87 |
+
if (models.find(m => m.id === modelId).available) {
|
88 |
+
onModelSelect(modelId);
|
89 |
+
}
|
90 |
+
};
|
91 |
+
|
92 |
+
return (
|
93 |
+
<Box sx={{ p: 2, height: '100%' }}>
|
94 |
+
<Typography variant="h6" gutterBottom>
|
95 |
+
Select Model
|
96 |
+
</Typography>
|
97 |
+
|
98 |
+
<Grid container spacing={2}>
|
99 |
+
{models.map((model) => (
|
100 |
+
<Grid item xs={12} sm={4} key={model.id}>
|
101 |
+
<Card
|
102 |
+
className={`
|
103 |
+
${classes.card}
|
104 |
+
${selectedModel === model.id ? classes.selectedCard : ''}
|
105 |
+
${!model.available ? classes.unavailableCard : ''}
|
106 |
+
`}
|
107 |
+
onClick={() => handleModelClick(model.id)}
|
108 |
+
>
|
109 |
+
<CardContent className={classes.cardContent}>
|
110 |
+
<Box sx={{ mb: 2, color: 'primary' }}>
|
111 |
+
{model.icon}
|
112 |
+
</Box>
|
113 |
+
<Typography variant="h5" component="div" gutterBottom>
|
114 |
+
{model.name}
|
115 |
+
</Typography>
|
116 |
+
<div className={classes.chipContainer}>
|
117 |
+
{model.available ? (
|
118 |
+
<Chip
|
119 |
+
label="Available"
|
120 |
+
className={classes.successChip}
|
121 |
+
size="small"
|
122 |
+
/>
|
123 |
+
) : (
|
124 |
+
<Chip
|
125 |
+
label="Not Available"
|
126 |
+
className={classes.errorChip}
|
127 |
+
size="small"
|
128 |
+
/>
|
129 |
+
)}
|
130 |
+
</div>
|
131 |
+
<Typography variant="body2" color="textSecondary">
|
132 |
+
{model.description}
|
133 |
+
</Typography>
|
134 |
+
</CardContent>
|
135 |
+
<CardActions>
|
136 |
+
<Button
|
137 |
+
size="small"
|
138 |
+
onClick={() => handleModelClick(model.id)}
|
139 |
+
disabled={!model.available}
|
140 |
+
color={selectedModel === model.id ? "primary" : "default"}
|
141 |
+
variant={selectedModel === model.id ? "contained" : "outlined"}
|
142 |
+
fullWidth
|
143 |
+
>
|
144 |
+
{selectedModel === model.id ? 'Selected' : 'Select'}
|
145 |
+
</Button>
|
146 |
+
</CardActions>
|
147 |
+
</Card>
|
148 |
+
</Grid>
|
149 |
+
))}
|
150 |
+
</Grid>
|
151 |
+
|
152 |
+
<div className={classes.processButton}>
|
153 |
+
<Button
|
154 |
+
variant="contained"
|
155 |
+
color="primary"
|
156 |
+
size="large"
|
157 |
+
startIcon={<PlayArrowIcon />}
|
158 |
+
onClick={onProcess}
|
159 |
+
disabled={!selectedModel || !imageSelected || isProcessing}
|
160 |
+
>
|
161 |
+
{isProcessing ? 'Processing...' : 'Process Image'}
|
162 |
+
</Button>
|
163 |
+
</div>
|
164 |
+
</Box>
|
165 |
+
);
|
166 |
+
};
|
167 |
+
|
168 |
+
export default ModelSelector;
|
frontend/src/components/OpenAIChat.js
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import {
|
3 |
+
Paper,
|
4 |
+
Typography,
|
5 |
+
Grid,
|
6 |
+
TextField,
|
7 |
+
Button,
|
8 |
+
Divider
|
9 |
+
} from '@material-ui/core';
|
10 |
+
|
11 |
+
function OpenAIChat() {
|
12 |
+
const [model, setModel] = useState('gpt-4o-mini');
|
13 |
+
const [apiKey, setApiKey] = useState('');
|
14 |
+
const [system, setSystem] = useState('You are a helpful assistant.');
|
15 |
+
const [prompt, setPrompt] = useState('');
|
16 |
+
const [response, setResponse] = useState('');
|
17 |
+
const [loading, setLoading] = useState(false);
|
18 |
+
const [error, setError] = useState('');
|
19 |
+
|
20 |
+
const onSend = async () => {
|
21 |
+
setError('');
|
22 |
+
setResponse('');
|
23 |
+
const p = (prompt || '').trim();
|
24 |
+
if (!p) { setError('Please enter a question.'); return; }
|
25 |
+
|
26 |
+
setLoading(true);
|
27 |
+
try {
|
28 |
+
const res = await fetch('/api/openai/chat', {
|
29 |
+
method: 'POST',
|
30 |
+
headers: { 'Content-Type': 'application/json' },
|
31 |
+
credentials: 'include',
|
32 |
+
body: JSON.stringify({
|
33 |
+
prompt: p,
|
34 |
+
model: (model || '').trim() || 'gpt-4o-mini',
|
35 |
+
api_key: (apiKey || undefined),
|
36 |
+
system: (system || undefined)
|
37 |
+
})
|
38 |
+
});
|
39 |
+
|
40 |
+
if (!res.ok) {
|
41 |
+
let txt = await res.text();
|
42 |
+
try { txt = JSON.stringify(JSON.parse(txt), null, 2); } catch {}
|
43 |
+
throw new Error(txt);
|
44 |
+
}
|
45 |
+
const data = await res.json();
|
46 |
+
const meta = `Model: ${data.model} | Latency: ${data.latency_sec}s` + (data.usage ? ` | Usage: ${JSON.stringify(data.usage)}` : '');
|
47 |
+
setResponse((data.response || '(Empty response)') + '\n\n---\n' + meta);
|
48 |
+
} catch (e) {
|
49 |
+
setError('Error: ' + e.message);
|
50 |
+
} finally {
|
51 |
+
setLoading(false);
|
52 |
+
}
|
53 |
+
};
|
54 |
+
|
55 |
+
const onClear = () => {
|
56 |
+
setPrompt('');
|
57 |
+
setResponse('');
|
58 |
+
setError('');
|
59 |
+
};
|
60 |
+
|
61 |
+
return (
|
62 |
+
<Paper style={{ padding: 16 }}>
|
63 |
+
<Typography variant="h5" gutterBottom>
|
64 |
+
OpenAI Chat (OpenAI API)
|
65 |
+
</Typography>
|
66 |
+
<Typography variant="body2" color="textSecondary" gutterBottom>
|
67 |
+
If the server env var OPENAI_API_KEY is set, the API Key field is optional.
|
68 |
+
</Typography>
|
69 |
+
<Grid container spacing={2}>
|
70 |
+
<Grid item xs={12} md={6}>
|
71 |
+
<TextField
|
72 |
+
label="Model"
|
73 |
+
value={model}
|
74 |
+
onChange={(e) => setModel(e.target.value)}
|
75 |
+
fullWidth
|
76 |
+
variant="outlined"
|
77 |
+
size="small"
|
78 |
+
/>
|
79 |
+
</Grid>
|
80 |
+
<Grid item xs={12} md={6}>
|
81 |
+
<TextField
|
82 |
+
label="OpenAI API Key (optional)"
|
83 |
+
value={apiKey}
|
84 |
+
onChange={(e) => setApiKey(e.target.value)}
|
85 |
+
fullWidth
|
86 |
+
variant="outlined"
|
87 |
+
size="small"
|
88 |
+
type="password"
|
89 |
+
placeholder="sk-..."
|
90 |
+
/>
|
91 |
+
</Grid>
|
92 |
+
<Grid item xs={12}>
|
93 |
+
<TextField
|
94 |
+
label="System Prompt (optional)"
|
95 |
+
value={system}
|
96 |
+
onChange={(e) => setSystem(e.target.value)}
|
97 |
+
fullWidth
|
98 |
+
variant="outlined"
|
99 |
+
size="small"
|
100 |
+
/>
|
101 |
+
</Grid>
|
102 |
+
<Grid item xs={12}>
|
103 |
+
<TextField
|
104 |
+
label="User Question"
|
105 |
+
value={prompt}
|
106 |
+
onChange={(e) => setPrompt(e.target.value)}
|
107 |
+
fullWidth
|
108 |
+
multiline
|
109 |
+
rows={4}
|
110 |
+
variant="outlined"
|
111 |
+
/>
|
112 |
+
</Grid>
|
113 |
+
{error && (
|
114 |
+
<Grid item xs={12}>
|
115 |
+
<Typography color="error">{error}</Typography>
|
116 |
+
</Grid>
|
117 |
+
)}
|
118 |
+
<Grid item xs={12}>
|
119 |
+
<div style={{ display: 'flex', gap: 8 }}>
|
120 |
+
<Button color="primary" variant="contained" onClick={onSend} disabled={loading}>
|
121 |
+
{loading ? 'Sending...' : 'Send Question'}
|
122 |
+
</Button>
|
123 |
+
<Button variant="outlined" onClick={onClear}>Clear</Button>
|
124 |
+
</div>
|
125 |
+
</Grid>
|
126 |
+
<Grid item xs={12}>
|
127 |
+
<Divider style={{ margin: '12px 0' }} />
|
128 |
+
<Typography variant="subtitle2" color="textSecondary">Response</Typography>
|
129 |
+
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'ui-monospace, monospace' }}>{response}</pre>
|
130 |
+
</Grid>
|
131 |
+
</Grid>
|
132 |
+
</Paper>
|
133 |
+
);
|
134 |
+
}
|
135 |
+
|
136 |
+
export default OpenAIChat;
|
frontend/src/components/ResultDisplay.js
ADDED
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import {
|
3 |
+
Paper,
|
4 |
+
Typography,
|
5 |
+
Box,
|
6 |
+
List,
|
7 |
+
ListItem,
|
8 |
+
ListItemText,
|
9 |
+
Divider,
|
10 |
+
Grid,
|
11 |
+
Chip
|
12 |
+
} from '@material-ui/core';
|
13 |
+
import VectorDBActions from './VectorDBActions';
|
14 |
+
import { makeStyles } from '@material-ui/core/styles';
|
15 |
+
|
16 |
+
const useStyles = makeStyles((theme) => ({
|
17 |
+
paper: {
|
18 |
+
padding: theme.spacing(2)
|
19 |
+
},
|
20 |
+
marginBottom: {
|
21 |
+
marginBottom: theme.spacing(2)
|
22 |
+
},
|
23 |
+
resultImage: {
|
24 |
+
maxWidth: '100%',
|
25 |
+
maxHeight: '400px',
|
26 |
+
objectFit: 'contain'
|
27 |
+
},
|
28 |
+
dividerMargin: {
|
29 |
+
margin: `${theme.spacing(2)}px 0`
|
30 |
+
},
|
31 |
+
chipContainer: {
|
32 |
+
display: 'flex',
|
33 |
+
gap: theme.spacing(1),
|
34 |
+
flexWrap: 'wrap'
|
35 |
+
}
|
36 |
+
}));
|
37 |
+
|
38 |
+
const ResultDisplay = ({ results }) => {
|
39 |
+
const classes = useStyles();
|
40 |
+
if (!results) return null;
|
41 |
+
|
42 |
+
const { model, data } = results;
|
43 |
+
|
44 |
+
// Helper to format times nicely
|
45 |
+
const formatTime = (ms) => {
|
46 |
+
if (ms === undefined || ms === null || isNaN(ms)) return '-';
|
47 |
+
const num = Number(ms);
|
48 |
+
if (num < 1000) return `${num.toFixed(2)} ms`;
|
49 |
+
return `${(num / 1000).toFixed(2)} s`;
|
50 |
+
};
|
51 |
+
|
52 |
+
// Check if there's an error
|
53 |
+
if (data.error) {
|
54 |
+
return (
|
55 |
+
<Paper sx={{ p: 2, bgcolor: '#ffebee' }}>
|
56 |
+
<Typography color="error">{data.error}</Typography>
|
57 |
+
</Paper>
|
58 |
+
);
|
59 |
+
}
|
60 |
+
|
61 |
+
// Display performance info
|
62 |
+
const renderPerformanceInfo = () => {
|
63 |
+
if (!data.performance) return null;
|
64 |
+
|
65 |
+
return (
|
66 |
+
<Box className="performance-info">
|
67 |
+
<Divider className={classes.dividerMargin} />
|
68 |
+
<Typography variant="body2">
|
69 |
+
Inference time: {formatTime(data.performance.inference_time)} on {data.performance.device}
|
70 |
+
</Typography>
|
71 |
+
</Box>
|
72 |
+
);
|
73 |
+
};
|
74 |
+
|
75 |
+
// Render for YOLO and DETR (object detection)
|
76 |
+
if (model === 'yolo' || model === 'detr') {
|
77 |
+
return (
|
78 |
+
<Paper className={classes.paper}>
|
79 |
+
<Typography variant="h6" gutterBottom>
|
80 |
+
{model === 'yolo' ? 'YOLOv8' : 'DETR'} Detection Results
|
81 |
+
</Typography>
|
82 |
+
|
83 |
+
<Grid container spacing={3}>
|
84 |
+
<Grid item xs={12} md={6}>
|
85 |
+
{data.image && (
|
86 |
+
<Box className={classes.marginBottom}>
|
87 |
+
<Typography variant="subtitle1" gutterBottom>
|
88 |
+
Detection Result
|
89 |
+
</Typography>
|
90 |
+
<img
|
91 |
+
src={`data:image/png;base64,${data.image}`}
|
92 |
+
alt="Detection Result"
|
93 |
+
className={classes.resultImage}
|
94 |
+
/>
|
95 |
+
</Box>
|
96 |
+
)}
|
97 |
+
</Grid>
|
98 |
+
|
99 |
+
<Grid item xs={12} md={6}>
|
100 |
+
<Box className={classes.marginBottom}>
|
101 |
+
<Typography variant="subtitle1" gutterBottom>
|
102 |
+
Detected Objects:
|
103 |
+
</Typography>
|
104 |
+
|
105 |
+
{data.detections && data.detections.length > 0 ? (
|
106 |
+
<List>
|
107 |
+
{data.detections.map((detection, index) => (
|
108 |
+
<React.Fragment key={index}>
|
109 |
+
<ListItem>
|
110 |
+
<ListItemText
|
111 |
+
primary={
|
112 |
+
<Box style={{ display: 'flex', alignItems: 'center' }}>
|
113 |
+
<Typography variant="body1" component="span">
|
114 |
+
{detection.class}
|
115 |
+
</Typography>
|
116 |
+
<Chip
|
117 |
+
label={`${(detection.confidence * 100).toFixed(0)}%`}
|
118 |
+
size="small"
|
119 |
+
color="primary"
|
120 |
+
style={{ marginLeft: 8 }}
|
121 |
+
/>
|
122 |
+
</Box>
|
123 |
+
}
|
124 |
+
secondary={`Bounding Box: [${detection.bbox.join(', ')}]`}
|
125 |
+
/>
|
126 |
+
</ListItem>
|
127 |
+
{index < data.detections.length - 1 && <Divider />}
|
128 |
+
</React.Fragment>
|
129 |
+
))}
|
130 |
+
</List>
|
131 |
+
) : (
|
132 |
+
<Typography variant="body1">No objects detected</Typography>
|
133 |
+
)}
|
134 |
+
</Box>
|
135 |
+
</Grid>
|
136 |
+
</Grid>
|
137 |
+
|
138 |
+
{renderPerformanceInfo()}
|
139 |
+
|
140 |
+
{/* Vector DB Actions for Object Detection */}
|
141 |
+
<VectorDBActions results={results} />
|
142 |
+
</Paper>
|
143 |
+
);
|
144 |
+
}
|
145 |
+
|
146 |
+
// Render for ViT (classification)
|
147 |
+
if (model === 'vit') {
|
148 |
+
return (
|
149 |
+
<Paper className={classes.paper}>
|
150 |
+
<Typography variant="h6" gutterBottom>
|
151 |
+
ViT Classification Results
|
152 |
+
</Typography>
|
153 |
+
|
154 |
+
<Typography variant="subtitle1" gutterBottom>
|
155 |
+
Top Predictions:
|
156 |
+
</Typography>
|
157 |
+
|
158 |
+
{data.top_predictions && data.top_predictions.length > 0 ? (
|
159 |
+
<List>
|
160 |
+
{data.top_predictions.map((prediction, index) => (
|
161 |
+
<React.Fragment key={index}>
|
162 |
+
<ListItem>
|
163 |
+
<ListItemText
|
164 |
+
primary={
|
165 |
+
<Box style={{ display: 'flex', alignItems: 'center' }}>
|
166 |
+
<Typography variant="body1" component="span">
|
167 |
+
{prediction.rank}. {prediction.class}
|
168 |
+
</Typography>
|
169 |
+
<Chip
|
170 |
+
label={`${(prediction.probability * 100).toFixed(1)}%`}
|
171 |
+
size="small"
|
172 |
+
color={index === 0 ? "primary" : "default"}
|
173 |
+
style={{ marginLeft: 8 }}
|
174 |
+
/>
|
175 |
+
</Box>
|
176 |
+
}
|
177 |
+
/>
|
178 |
+
</ListItem>
|
179 |
+
{index < data.top_predictions.length - 1 && <Divider />}
|
180 |
+
</React.Fragment>
|
181 |
+
))}
|
182 |
+
</List>
|
183 |
+
) : (
|
184 |
+
<Typography variant="body1">No classifications available</Typography>
|
185 |
+
)}
|
186 |
+
|
187 |
+
{renderPerformanceInfo()}
|
188 |
+
|
189 |
+
{/* Vector DB Actions for ViT Classification */}
|
190 |
+
<VectorDBActions results={results} />
|
191 |
+
</Paper>
|
192 |
+
);
|
193 |
+
}
|
194 |
+
|
195 |
+
return null;
|
196 |
+
};
|
197 |
+
|
198 |
+
export default ResultDisplay;
|
frontend/src/components/VectorDBActions.js
ADDED
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import {
|
3 |
+
Button,
|
4 |
+
Box,
|
5 |
+
Typography,
|
6 |
+
CircularProgress,
|
7 |
+
Snackbar,
|
8 |
+
Dialog,
|
9 |
+
DialogTitle,
|
10 |
+
DialogContent,
|
11 |
+
DialogActions,
|
12 |
+
TextField,
|
13 |
+
FormControl,
|
14 |
+
InputLabel,
|
15 |
+
Select,
|
16 |
+
MenuItem,
|
17 |
+
Grid,
|
18 |
+
Card,
|
19 |
+
CardMedia,
|
20 |
+
CardContent,
|
21 |
+
Chip
|
22 |
+
} from '@material-ui/core';
|
23 |
+
import { Alert } from '@material-ui/lab';
|
24 |
+
import { makeStyles } from '@material-ui/core/styles';
|
25 |
+
|
26 |
+
const useStyles = makeStyles((theme) => ({
|
27 |
+
root: {
|
28 |
+
marginTop: theme.spacing(2),
|
29 |
+
marginBottom: theme.spacing(2),
|
30 |
+
padding: theme.spacing(2),
|
31 |
+
backgroundColor: '#f5f5f5',
|
32 |
+
borderRadius: theme.shape.borderRadius,
|
33 |
+
},
|
34 |
+
button: {
|
35 |
+
marginRight: theme.spacing(2),
|
36 |
+
},
|
37 |
+
searchDialog: {
|
38 |
+
minWidth: '500px',
|
39 |
+
},
|
40 |
+
formControl: {
|
41 |
+
marginBottom: theme.spacing(2),
|
42 |
+
minWidth: '100%',
|
43 |
+
},
|
44 |
+
searchResults: {
|
45 |
+
marginTop: theme.spacing(2),
|
46 |
+
},
|
47 |
+
resultCard: {
|
48 |
+
marginBottom: theme.spacing(2),
|
49 |
+
},
|
50 |
+
resultImage: {
|
51 |
+
height: 140,
|
52 |
+
objectFit: 'contain',
|
53 |
+
},
|
54 |
+
chip: {
|
55 |
+
margin: theme.spacing(0.5),
|
56 |
+
},
|
57 |
+
similarityChip: {
|
58 |
+
backgroundColor: theme.palette.primary.main,
|
59 |
+
color: 'white',
|
60 |
+
}
|
61 |
+
}));
|
62 |
+
|
63 |
+
const VectorDBActions = ({ results }) => {
|
64 |
+
const classes = useStyles();
|
65 |
+
const [isSaving, setIsSaving] = useState(false);
|
66 |
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
67 |
+
const [saveError, setSaveError] = useState(null);
|
68 |
+
const [openSearchDialog, setOpenSearchDialog] = useState(false);
|
69 |
+
const [searchType, setSearchType] = useState('image');
|
70 |
+
const [searchClass, setSearchClass] = useState('');
|
71 |
+
const [searchResults, setSearchResults] = useState([]);
|
72 |
+
const [isSearching, setIsSearching] = useState(false);
|
73 |
+
const [searchError, setSearchError] = useState(null);
|
74 |
+
|
75 |
+
// Extract model and data from results
|
76 |
+
const { model, data } = results;
|
77 |
+
|
78 |
+
// Handle saving to vector DB
|
79 |
+
const handleSaveToVectorDB = async () => {
|
80 |
+
setIsSaving(true);
|
81 |
+
setSaveError(null);
|
82 |
+
|
83 |
+
try {
|
84 |
+
let response;
|
85 |
+
|
86 |
+
if (model === 'vit') {
|
87 |
+
// For ViT, save the whole image with classifications
|
88 |
+
response = await fetch('/api/add-to-collection', {
|
89 |
+
method: 'POST',
|
90 |
+
headers: {
|
91 |
+
'Content-Type': 'application/json',
|
92 |
+
},
|
93 |
+
body: JSON.stringify({
|
94 |
+
image: data.image,
|
95 |
+
metadata: {
|
96 |
+
model: 'vit',
|
97 |
+
classifications: data.classifications
|
98 |
+
}
|
99 |
+
})
|
100 |
+
});
|
101 |
+
} else {
|
102 |
+
// For YOLO and DETR, save detected objects
|
103 |
+
response = await fetch('/api/add-detected-objects', {
|
104 |
+
method: 'POST',
|
105 |
+
headers: {
|
106 |
+
'Content-Type': 'application/json',
|
107 |
+
},
|
108 |
+
body: JSON.stringify({
|
109 |
+
image: data.image,
|
110 |
+
objects: data.detections,
|
111 |
+
imageId: generateUUID()
|
112 |
+
})
|
113 |
+
});
|
114 |
+
}
|
115 |
+
|
116 |
+
if (!response.ok) {
|
117 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
118 |
+
}
|
119 |
+
|
120 |
+
const result = await response.json();
|
121 |
+
|
122 |
+
if (result.error) {
|
123 |
+
throw new Error(result.error);
|
124 |
+
}
|
125 |
+
|
126 |
+
setSaveSuccess(true);
|
127 |
+
setTimeout(() => setSaveSuccess(false), 5000);
|
128 |
+
} catch (err) {
|
129 |
+
console.error('Error saving to vector DB:', err);
|
130 |
+
setSaveError(`Error saving to vector DB: ${err.message}`);
|
131 |
+
} finally {
|
132 |
+
setIsSaving(false);
|
133 |
+
}
|
134 |
+
};
|
135 |
+
|
136 |
+
// Handle opening search dialog
|
137 |
+
const handleOpenSearchDialog = () => {
|
138 |
+
setOpenSearchDialog(true);
|
139 |
+
setSearchResults([]);
|
140 |
+
setSearchError(null);
|
141 |
+
};
|
142 |
+
|
143 |
+
// Handle closing search dialog
|
144 |
+
const handleCloseSearchDialog = () => {
|
145 |
+
setOpenSearchDialog(false);
|
146 |
+
};
|
147 |
+
|
148 |
+
// Handle search type change
|
149 |
+
const handleSearchTypeChange = (event) => {
|
150 |
+
setSearchType(event.target.value);
|
151 |
+
setSearchResults([]);
|
152 |
+
setSearchError(null);
|
153 |
+
};
|
154 |
+
|
155 |
+
// Handle search class change
|
156 |
+
const handleSearchClassChange = (event) => {
|
157 |
+
setSearchClass(event.target.value);
|
158 |
+
};
|
159 |
+
|
160 |
+
// Handle search
|
161 |
+
const handleSearch = async () => {
|
162 |
+
setIsSearching(true);
|
163 |
+
setSearchError(null);
|
164 |
+
|
165 |
+
try {
|
166 |
+
let requestBody = {};
|
167 |
+
|
168 |
+
if (searchType === 'image') {
|
169 |
+
// Search by current image
|
170 |
+
requestBody = {
|
171 |
+
searchType: 'image',
|
172 |
+
image: data.image,
|
173 |
+
n_results: 5
|
174 |
+
};
|
175 |
+
} else {
|
176 |
+
// Search by class name
|
177 |
+
if (!searchClass.trim()) {
|
178 |
+
throw new Error('Please enter a class name');
|
179 |
+
}
|
180 |
+
|
181 |
+
requestBody = {
|
182 |
+
searchType: 'class',
|
183 |
+
class_name: searchClass.trim(),
|
184 |
+
n_results: 5
|
185 |
+
};
|
186 |
+
}
|
187 |
+
|
188 |
+
const response = await fetch('/api/search-similar-objects', {
|
189 |
+
method: 'POST',
|
190 |
+
headers: {
|
191 |
+
'Content-Type': 'application/json',
|
192 |
+
},
|
193 |
+
body: JSON.stringify(requestBody)
|
194 |
+
});
|
195 |
+
|
196 |
+
if (!response.ok) {
|
197 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
198 |
+
}
|
199 |
+
|
200 |
+
const result = await response.json();
|
201 |
+
|
202 |
+
if (result.error) {
|
203 |
+
throw new Error(result.error);
|
204 |
+
}
|
205 |
+
|
206 |
+
console.log('Search API response:', result);
|
207 |
+
|
208 |
+
// The backend responds with {success, searchType, results} structure, so extract only the results array
|
209 |
+
if (result.success && Array.isArray(result.results)) {
|
210 |
+
console.log('Setting search results array:', result.results);
|
211 |
+
console.log('Results array length:', result.results.length);
|
212 |
+
console.log('First result item:', result.results[0]);
|
213 |
+
setSearchResults(result.results);
|
214 |
+
} else {
|
215 |
+
console.error('Unexpected API response format:', result);
|
216 |
+
throw new Error('Unexpected API response format');
|
217 |
+
}
|
218 |
+
} catch (err) {
|
219 |
+
console.error('Error searching vector DB:', err);
|
220 |
+
setSearchError(`Error searching vector DB: ${err.message}`);
|
221 |
+
} finally {
|
222 |
+
setIsSearching(false);
|
223 |
+
}
|
224 |
+
};
|
225 |
+
|
226 |
+
// Generate UUID for image ID
|
227 |
+
const generateUUID = () => {
|
228 |
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
229 |
+
const r = Math.random() * 16 | 0;
|
230 |
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
231 |
+
return v.toString(16);
|
232 |
+
});
|
233 |
+
};
|
234 |
+
|
235 |
+
// Render search results
|
236 |
+
const renderSearchResults = () => {
|
237 |
+
console.log('Rendering search results:', searchResults);
|
238 |
+
console.log('Search results length:', searchResults.length);
|
239 |
+
|
240 |
+
if (searchResults.length === 0) {
|
241 |
+
console.log('No results to render');
|
242 |
+
return (
|
243 |
+
<Typography variant="body1">No results found.</Typography>
|
244 |
+
);
|
245 |
+
}
|
246 |
+
|
247 |
+
return (
|
248 |
+
<Grid container spacing={2}>
|
249 |
+
{searchResults.map((result, index) => {
|
250 |
+
const similarity = (1 - result.distance) * 100;
|
251 |
+
|
252 |
+
return (
|
253 |
+
<Grid item xs={12} sm={6} key={index}>
|
254 |
+
<Card className={classes.resultCard}>
|
255 |
+
{result.metadata && result.metadata.image_data ? (
|
256 |
+
<CardMedia
|
257 |
+
className={classes.resultImage}
|
258 |
+
component="img"
|
259 |
+
height="200"
|
260 |
+
image={`data:image/jpeg;base64,${result.metadata.image_data}`}
|
261 |
+
alt={result.metadata && result.metadata.class ? result.metadata.class : 'Object'}
|
262 |
+
/>
|
263 |
+
) : (
|
264 |
+
<Box
|
265 |
+
className={classes.resultImage}
|
266 |
+
style={{
|
267 |
+
backgroundColor: '#f0f0f0',
|
268 |
+
display: 'flex',
|
269 |
+
alignItems: 'center',
|
270 |
+
justifyContent: 'center',
|
271 |
+
height: 200
|
272 |
+
}}
|
273 |
+
>
|
274 |
+
<Typography variant="body2" color="textSecondary">
|
275 |
+
{result.metadata && result.metadata.class ? result.metadata.class : 'Object'} Image
|
276 |
+
</Typography>
|
277 |
+
</Box>
|
278 |
+
)}
|
279 |
+
<CardContent>
|
280 |
+
<Box display="flex" justifyContent="space-between" alignItems="center" mb={1}>
|
281 |
+
<Typography variant="subtitle1">Result #{index + 1}</Typography>
|
282 |
+
<Chip
|
283 |
+
label={`Similarity: ${similarity.toFixed(2)}%`}
|
284 |
+
className={classes.similarityChip}
|
285 |
+
size="small"
|
286 |
+
/>
|
287 |
+
</Box>
|
288 |
+
<Typography variant="body2" color="textSecondary">
|
289 |
+
<strong>Class:</strong> {result.metadata.class || 'N/A'}
|
290 |
+
</Typography>
|
291 |
+
{result.metadata.confidence && (
|
292 |
+
<Typography variant="body2" color="textSecondary">
|
293 |
+
<strong>Confidence:</strong> {(result.metadata.confidence * 100).toFixed(2)}%
|
294 |
+
</Typography>
|
295 |
+
)}
|
296 |
+
<Typography variant="body2" color="textSecondary">
|
297 |
+
<strong>Object ID:</strong> {result.id}
|
298 |
+
</Typography>
|
299 |
+
</CardContent>
|
300 |
+
</Card>
|
301 |
+
</Grid>
|
302 |
+
);
|
303 |
+
})}
|
304 |
+
</Grid>
|
305 |
+
);
|
306 |
+
};
|
307 |
+
|
308 |
+
return (
|
309 |
+
<Box className={classes.root}>
|
310 |
+
<Typography variant="h6" gutterBottom>
|
311 |
+
Vector Database Actions
|
312 |
+
</Typography>
|
313 |
+
|
314 |
+
<Box display="flex" alignItems="center" mb={2}>
|
315 |
+
<Button
|
316 |
+
variant="contained"
|
317 |
+
color="primary"
|
318 |
+
onClick={handleSaveToVectorDB}
|
319 |
+
disabled={isSaving}
|
320 |
+
className={classes.button}
|
321 |
+
>
|
322 |
+
{isSaving ? (
|
323 |
+
<>
|
324 |
+
<CircularProgress size={20} color="inherit" style={{ marginRight: 8 }} />
|
325 |
+
Saving...
|
326 |
+
</>
|
327 |
+
) : (
|
328 |
+
'Save to Vector DB'
|
329 |
+
)}
|
330 |
+
</Button>
|
331 |
+
|
332 |
+
<Button
|
333 |
+
variant="outlined"
|
334 |
+
color="primary"
|
335 |
+
onClick={handleOpenSearchDialog}
|
336 |
+
className={classes.button}
|
337 |
+
>
|
338 |
+
Search Similar
|
339 |
+
</Button>
|
340 |
+
</Box>
|
341 |
+
|
342 |
+
{saveError && (
|
343 |
+
<Alert severity="error" style={{ marginTop: 8 }}>
|
344 |
+
{saveError}
|
345 |
+
</Alert>
|
346 |
+
)}
|
347 |
+
|
348 |
+
<Snackbar open={saveSuccess} autoHideDuration={5000} onClose={() => setSaveSuccess(false)}>
|
349 |
+
<Alert severity="success">
|
350 |
+
{model === 'vit' ? (
|
351 |
+
'Image and classifications successfully saved to vector DB!'
|
352 |
+
) : (
|
353 |
+
'Detected objects successfully saved to vector DB!'
|
354 |
+
)}
|
355 |
+
</Alert>
|
356 |
+
</Snackbar>
|
357 |
+
|
358 |
+
{/* Search Dialog */}
|
359 |
+
<Dialog
|
360 |
+
open={openSearchDialog}
|
361 |
+
onClose={handleCloseSearchDialog}
|
362 |
+
maxWidth="md"
|
363 |
+
fullWidth
|
364 |
+
>
|
365 |
+
<DialogTitle>Search Vector Database</DialogTitle>
|
366 |
+
<DialogContent>
|
367 |
+
<FormControl className={classes.formControl}>
|
368 |
+
<InputLabel id="search-type-label">Search Type</InputLabel>
|
369 |
+
<Select
|
370 |
+
labelId="search-type-label"
|
371 |
+
id="search-type"
|
372 |
+
value={searchType}
|
373 |
+
onChange={handleSearchTypeChange}
|
374 |
+
>
|
375 |
+
<MenuItem value="image">Search by Current Image</MenuItem>
|
376 |
+
<MenuItem value="class">Search by Class Name</MenuItem>
|
377 |
+
</Select>
|
378 |
+
</FormControl>
|
379 |
+
|
380 |
+
{searchType === 'class' && (
|
381 |
+
<FormControl className={classes.formControl}>
|
382 |
+
<TextField
|
383 |
+
label="Class Name"
|
384 |
+
value={searchClass}
|
385 |
+
onChange={handleSearchClassChange}
|
386 |
+
placeholder="e.g. person, car, dog..."
|
387 |
+
fullWidth
|
388 |
+
/>
|
389 |
+
</FormControl>
|
390 |
+
)}
|
391 |
+
|
392 |
+
{searchError && (
|
393 |
+
<Alert severity="error" style={{ marginBottom: 16 }}>
|
394 |
+
{searchError}
|
395 |
+
</Alert>
|
396 |
+
)}
|
397 |
+
|
398 |
+
<Box className={classes.searchResults}>
|
399 |
+
{isSearching ? (
|
400 |
+
<Box display="flex" justifyContent="center" alignItems="center" p={4}>
|
401 |
+
<CircularProgress />
|
402 |
+
<Typography variant="body1" style={{ marginLeft: 16 }}>
|
403 |
+
Searching...
|
404 |
+
</Typography>
|
405 |
+
</Box>
|
406 |
+
) : (
|
407 |
+
<>
|
408 |
+
{console.log('Search dialog render - searchResults:', searchResults)}
|
409 |
+
{searchResults.length > 0 ? renderSearchResults() :
|
410 |
+
<Typography variant="body1">No results found. Please try another search.</Typography>
|
411 |
+
}
|
412 |
+
</>
|
413 |
+
)}
|
414 |
+
</Box>
|
415 |
+
</DialogContent>
|
416 |
+
<DialogActions>
|
417 |
+
<Button onClick={handleCloseSearchDialog} color="default">
|
418 |
+
Close
|
419 |
+
</Button>
|
420 |
+
<Button
|
421 |
+
onClick={handleSearch}
|
422 |
+
color="primary"
|
423 |
+
variant="contained"
|
424 |
+
disabled={isSearching || (searchType === 'class' && !searchClass.trim())}
|
425 |
+
>
|
426 |
+
Search
|
427 |
+
</Button>
|
428 |
+
</DialogActions>
|
429 |
+
</Dialog>
|
430 |
+
</Box>
|
431 |
+
);
|
432 |
+
};
|
433 |
+
|
434 |
+
export default VectorDBActions;
|
frontend/src/index.css
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
margin: 0;
|
3 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
4 |
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
5 |
+
sans-serif;
|
6 |
+
-webkit-font-smoothing: antialiased;
|
7 |
+
-moz-osx-font-smoothing: grayscale;
|
8 |
+
background-color: #f5f5f5;
|
9 |
+
}
|
10 |
+
|
11 |
+
code {
|
12 |
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
13 |
+
monospace;
|
14 |
+
}
|
frontend/src/index.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import ReactDOM from 'react-dom';
|
3 |
+
import './index.css';
|
4 |
+
import App from './App';
|
5 |
+
import reportWebVitals from './reportWebVitals';
|
6 |
+
|
7 |
+
ReactDOM.render(
|
8 |
+
<React.StrictMode>
|
9 |
+
<App />
|
10 |
+
</React.StrictMode>,
|
11 |
+
document.getElementById('root')
|
12 |
+
);
|
13 |
+
|
14 |
+
// If you want to start measuring performance in your app, pass a function
|
15 |
+
// to log results (for example: reportWebVitals(console.log))
|
16 |
+
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
17 |
+
reportWebVitals();
|
frontend/src/reportWebVitals.js
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const reportWebVitals = (onPerfEntry) => {
|
2 |
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
3 |
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
4 |
+
getCLS(onPerfEntry);
|
5 |
+
getFID(onPerfEntry);
|
6 |
+
getFCP(onPerfEntry);
|
7 |
+
getLCP(onPerfEntry);
|
8 |
+
getTTFB(onPerfEntry);
|
9 |
+
});
|
10 |
+
}
|
11 |
+
};
|
12 |
+
|
13 |
+
export default reportWebVitals;
|