Sunhey Cho commited on
Commit
9aee46b
·
1 Parent(s): 0e9cdf3

Add frontend source code structure for direct development

Browse files
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;