ChandraP12330 commited on
Commit
da32222
·
verified ·
1 Parent(s): ef09de9

Upload 16 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .git
4
+ .gitignore
5
+ README.md
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Node runtime as a parent image
2
+ FROM node:20-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy package.json and package-lock.json
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm ci
12
+
13
+ # Copy the rest of the application code
14
+ COPY . .
15
+
16
+ # Build the app
17
+ RUN npm run build
18
+
19
+ # Install 'serve' to serve the static files
20
+ RUN npm install -g serve
21
+
22
+ # Expose port 7860 (Hugging Face Spaces default)
23
+ EXPOSE 7860
24
+
25
+ # Command to run the app
26
+ CMD ["serve", "-s", "dist", "-l", "7860"]
eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.13.2",
14
+ "react": "^19.2.0",
15
+ "react-dom": "^19.2.0",
16
+ "react-markdown": "^10.1.0",
17
+ "remark-gfm": "^4.0.1"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.39.1",
21
+ "@types/react": "^19.2.5",
22
+ "@types/react-dom": "^19.2.3",
23
+ "@vitejs/plugin-react": "^5.1.1",
24
+ "eslint": "^9.39.1",
25
+ "eslint-plugin-react-hooks": "^7.0.1",
26
+ "eslint-plugin-react-refresh": "^0.4.24",
27
+ "globals": "^16.5.0",
28
+ "vite": "^7.2.4"
29
+ }
30
+ }
public/vite.svg ADDED
src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
src/App.jsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import axios from 'axios';
3
+ import ImageInput from './components/ImageInput';
4
+ import ResultsTable from './components/ResultsTable';
5
+
6
+ function App() {
7
+ const [url, setUrl] = useState('');
8
+ const [result, setResult] = useState(null);
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState(null);
11
+
12
+ const handleAnalyze = async () => {
13
+ if (!url) return;
14
+
15
+ setLoading(true);
16
+ setError(null);
17
+ setResult(null);
18
+
19
+ try {
20
+ const response = await axios.post('https://chandrap12330-vinm-base64.hf.space/analyze_shelf', {
21
+ image_base64: url
22
+ });
23
+ setResult(response.data.markdown_output);
24
+ } catch (err) {
25
+ console.error(err);
26
+ setError(err.response?.data?.detail?.[0]?.msg || 'Failed to analyze image. Please check the URL and try again.');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="app-container">
34
+ <h1>Shelf Analyzer</h1>
35
+
36
+ <ImageInput
37
+ onImageSelected={setUrl}
38
+ onAnalyze={handleAnalyze}
39
+ isLoading={loading}
40
+ hasResult={!!result}
41
+ />
42
+
43
+ {error && (
44
+ <div className="error-message">
45
+ {error}
46
+ </div>
47
+ )}
48
+
49
+ {result && (
50
+ <>
51
+ <div className="success-message">
52
+ <span>✅</span> Image Analysis completed successfully
53
+ </div>
54
+ <ResultsTable data={result} />
55
+ </>
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export default App;
src/assets/react.svg ADDED
src/components/ImageInput.jsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+
3
+ const ImageInput = ({ onImageSelected, onAnalyze, isLoading, hasResult }) => {
4
+ const [preview, setPreview] = useState(null);
5
+ const [fileName, setFileName] = useState('');
6
+
7
+ const handleFileChange = (e) => {
8
+ const file = e.target.files[0];
9
+ if (file) {
10
+ setFileName(file.name);
11
+ const reader = new FileReader();
12
+ reader.onloadend = () => {
13
+ const base64String = reader.result;
14
+ setPreview(base64String);
15
+ // Extract the raw base64 string (remove "data:image/jpeg;base64," prefix)
16
+ const rawBase64 = base64String.split(',')[1];
17
+ onImageSelected(rawBase64);
18
+ };
19
+ reader.readAsDataURL(file);
20
+ }
21
+ };
22
+
23
+ return (
24
+ <div className="card">
25
+ <div className="input-group" style={{ flexDirection: 'column', gap: '1rem' }}>
26
+ <div className="file-input-wrapper">
27
+ <input
28
+ type="file"
29
+ accept="image/*"
30
+ onChange={handleFileChange}
31
+ disabled={isLoading}
32
+ className="file-input"
33
+ id="file-upload"
34
+ />
35
+ <label htmlFor="file-upload" className="file-input-label">
36
+ <span>Choose File</span>
37
+ {fileName || 'No file chosen'}
38
+ </label>
39
+ </div>
40
+
41
+ {preview && (
42
+ <div className="image-preview" style={{ textAlign: 'center' }}>
43
+ <img
44
+ src={preview}
45
+ alt="Preview"
46
+ style={{ maxWidth: '100%', maxHeight: '400px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}
47
+ />
48
+ </div>
49
+ )}
50
+
51
+ <button
52
+ onClick={onAnalyze}
53
+ disabled={isLoading || !preview || hasResult}
54
+ style={{ width: '100%' }}
55
+ >
56
+ {isLoading ? <div className="loading-spinner" /> : 'Analyze Shelf'}
57
+ </button>
58
+ </div>
59
+ </div>
60
+ );
61
+ };
62
+
63
+ export default ImageInput;
src/components/ResultsTable.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+
5
+ const ResultsTable = ({ data }) => {
6
+ if (!data) return null;
7
+
8
+ return (
9
+ <div className="card">
10
+ <div className="table-container">
11
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
12
+ {data}
13
+ </ReactMarkdown>
14
+ </div>
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default ResultsTable;
src/components/UrlInput.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const UrlInput = ({ url, setUrl, onAnalyze, isLoading }) => {
4
+ return (
5
+ <div className="card">
6
+ <div className="input-group">
7
+ <input
8
+ type="text"
9
+ placeholder="Paste image URL here..."
10
+ value={url}
11
+ onChange={(e) => setUrl(e.target.value)}
12
+ disabled={isLoading}
13
+ />
14
+ <button onClick={onAnalyze} disabled={isLoading || !url.trim()}>
15
+ {isLoading ? <div className="loading-spinner" /> : 'Analyze Shelf'}
16
+ </button>
17
+ </div>
18
+ </div>
19
+ );
20
+ };
21
+
22
+ export default UrlInput;
src/index.css ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #6366f1;
3
+ /* Indigo 500 */
4
+ --primary-hover: #4f46e5;
5
+ /* Indigo 600 */
6
+ --background-dark: #0f172a;
7
+ /* Slate 900 */
8
+ --surface-dark: #1e293b;
9
+ /* Slate 800 */
10
+ --text-primary: #f8fafc;
11
+ /* Slate 50 */
12
+ --text-secondary: #94a3b8;
13
+ /* Slate 400 */
14
+ --accent-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
15
+ --glass-bg: rgba(30, 41, 59, 0.7);
16
+ --glass-border: rgba(255, 255, 255, 0.1);
17
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
23
+ background-color: var(--background-dark);
24
+ color: var(--text-primary);
25
+ -webkit-font-smoothing: antialiased;
26
+ min-height: 100vh;
27
+ display: flex;
28
+ justify-content: center;
29
+ }
30
+
31
+ #root {
32
+ width: 100%;
33
+ max-width: 1280px;
34
+ /* Increased from 480px */
35
+ padding: 1rem;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ /* Typography */
40
+ h1 {
41
+ font-size: 2rem;
42
+ font-weight: 800;
43
+ text-align: center;
44
+ margin-bottom: 2rem;
45
+ background: var(--accent-gradient);
46
+ background-clip: text;
47
+ -webkit-background-clip: text;
48
+ -webkit-text-fill-color: transparent;
49
+ letter-spacing: -0.025em;
50
+ }
51
+
52
+ /* Card Container */
53
+ .card {
54
+ background: var(--glass-bg);
55
+ backdrop-filter: blur(12px);
56
+ -webkit-backdrop-filter: blur(12px);
57
+ border: 1px solid var(--glass-border);
58
+ border-radius: 1.5rem;
59
+ padding: 1.5rem;
60
+ box-shadow: var(--shadow-lg);
61
+ margin-bottom: 1.5rem;
62
+ }
63
+
64
+ /* Input Group */
65
+ .input-group {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 1rem;
69
+ }
70
+
71
+ input[type="text"] {
72
+ width: 100%;
73
+ padding: 1rem;
74
+ border-radius: 1rem;
75
+ border: 1px solid var(--glass-border);
76
+ background: rgba(15, 23, 42, 0.6);
77
+ color: white;
78
+ font-size: 1rem;
79
+ transition: all 0.3s ease;
80
+ box-sizing: border-box;
81
+ }
82
+
83
+ input[type="text"]:focus {
84
+ outline: none;
85
+ border-color: var(--primary-color);
86
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
87
+ }
88
+
89
+ /* Modern File Input */
90
+ .file-input-wrapper {
91
+ position: relative;
92
+ width: 100%;
93
+ height: 3rem;
94
+ border: 1px dashed var(--glass-border);
95
+ border-radius: 1rem;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ background: rgba(15, 23, 42, 0.3);
100
+ transition: all 0.3s ease;
101
+ cursor: pointer;
102
+ overflow: hidden;
103
+ }
104
+
105
+ .file-input-wrapper:hover {
106
+ border-color: var(--primary-color);
107
+ background: rgba(15, 23, 42, 0.5);
108
+ }
109
+
110
+ .file-input {
111
+ position: absolute;
112
+ top: 0;
113
+ left: 0;
114
+ width: 100%;
115
+ height: 100%;
116
+ opacity: 0;
117
+ cursor: pointer;
118
+ }
119
+
120
+ .file-input-label {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.5rem;
124
+ color: var(--text-secondary);
125
+ font-size: 0.9rem;
126
+ pointer-events: none;
127
+ }
128
+
129
+ .file-input-label span {
130
+ color: var(--primary-color);
131
+ font-weight: 600;
132
+ }
133
+
134
+ /* Button */
135
+ button {
136
+ width: 100%;
137
+ padding: 1rem;
138
+ border-radius: 1rem;
139
+ border: none;
140
+ background: var(--accent-gradient);
141
+ color: white;
142
+ font-size: 1rem;
143
+ font-weight: 600;
144
+ cursor: pointer;
145
+ transition: transform 0.2s, box-shadow 0.2s;
146
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
147
+ }
148
+
149
+ button:active {
150
+ transform: scale(0.98);
151
+ }
152
+
153
+ button:disabled {
154
+ opacity: 0.7;
155
+ cursor: not-allowed;
156
+ filter: grayscale(0.5);
157
+ }
158
+
159
+ /* Table Styles */
160
+ .table-container {
161
+ overflow-x: auto;
162
+ border-radius: 1rem;
163
+ }
164
+
165
+ table {
166
+ width: 100%;
167
+ border-collapse: collapse;
168
+ font-size: 0.9rem;
169
+ }
170
+
171
+ th,
172
+ td {
173
+ padding: 0.75rem 1rem;
174
+ text-align: left;
175
+ border-bottom: 1px solid var(--glass-border);
176
+ }
177
+
178
+ th {
179
+ background: rgba(255, 255, 255, 0.05);
180
+ color: var(--text-secondary);
181
+ font-weight: 600;
182
+ text-transform: uppercase;
183
+ font-size: 0.75rem;
184
+ letter-spacing: 0.05em;
185
+ }
186
+
187
+ tr:last-child td {
188
+ border-bottom: none;
189
+ }
190
+
191
+ /* Loading Animation */
192
+ .loading-spinner {
193
+ width: 24px;
194
+ height: 24px;
195
+ border: 3px solid rgba(255, 255, 255, 0.3);
196
+ border-radius: 50%;
197
+ border-top-color: white;
198
+ animation: spin 1s ease-in-out infinite;
199
+ margin: 0 auto;
200
+ }
201
+
202
+ @keyframes spin {
203
+ to {
204
+ transform: rotate(360deg);
205
+ }
206
+ }
207
+
208
+ /* Error Message */
209
+ .error-message {
210
+ background: rgba(239, 68, 68, 0.1);
211
+ border: 1px solid rgba(239, 68, 68, 0.2);
212
+ color: #fca5a5;
213
+ padding: 1rem;
214
+ border-radius: 1rem;
215
+ text-align: center;
216
+ margin-top: 1rem;
217
+ font-size: 0.9rem;
218
+ }
219
+
220
+ /* Success Message */
221
+ .success-message {
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ gap: 0.5rem;
226
+ background: rgba(34, 197, 94, 0.1);
227
+ border: 1px solid rgba(34, 197, 94, 0.2);
228
+ color: #4ade80;
229
+ padding: 1rem;
230
+ border-radius: 1rem;
231
+ text-align: center;
232
+ margin-bottom: 1.5rem;
233
+ font-weight: 500;
234
+ }
src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })