Seym0n commited on
Commit
ea25548
·
1 Parent(s): cb2d036

feat: caption copy

Browse files
src/components/ImageAnalysisView.tsx CHANGED
@@ -18,6 +18,8 @@ export default function ImageAnalysisView({ images, onBackToUpload }: ImageAnaly
18
  const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
19
  const [currentImageIndex, setCurrentImageIndex] = useState<number>(0);
20
  const [selectedImageUrl, setSelectedImageUrl] = useState<string>("");
 
 
21
 
22
  const { isLoaded, runInference } = useVLMContext();
23
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -74,11 +76,57 @@ export default function ImageAnalysisView({ images, onBackToUpload }: ImageAnaly
74
  setCurrentImageIndex(index);
75
  }, []);
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const stopAnalysis = useCallback(() => {
78
  abortControllerRef.current?.abort();
79
  setIsAnalyzing(false);
80
  }, []);
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  useEffect(() => {
83
  return () => {
84
  abortControllerRef.current?.abort();
@@ -123,6 +171,42 @@ export default function ImageAnalysisView({ images, onBackToUpload }: ImageAnaly
123
  )}
124
  </div>
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  {isAnalyzing && (
127
  <div className="text-sm text-white/70 text-center">
128
  Analyzing image {currentImageIndex + 1} of {images.length}...
@@ -179,8 +263,48 @@ export default function ImageAnalysisView({ images, onBackToUpload }: ImageAnaly
179
  Error: {result.error}
180
  </div>
181
  ) : (
182
- <div className="text-white/80">
183
- {result.caption}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </div>
185
  )}
186
  </div>
 
18
  const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
19
  const [currentImageIndex, setCurrentImageIndex] = useState<number>(0);
20
  const [selectedImageUrl, setSelectedImageUrl] = useState<string>("");
21
+ const [copyStatus, setCopyStatus] = useState<{ [key: string]: 'success' | 'error' | null }>({});
22
+ const [copyAllStatus, setCopyAllStatus] = useState<'success' | 'error' | null>(null);
23
 
24
  const { isLoaded, runInference } = useVLMContext();
25
  const abortControllerRef = useRef<AbortController | null>(null);
 
76
  setCurrentImageIndex(index);
77
  }, []);
78
 
79
+ const copyToClipboard = useCallback(async (text: string, itemKey?: string) => {
80
+ try {
81
+ await navigator.clipboard.writeText(text);
82
+ if (itemKey) {
83
+ setCopyStatus(prev => ({ ...prev, [itemKey]: 'success' }));
84
+ setTimeout(() => setCopyStatus(prev => ({ ...prev, [itemKey]: null })), 2000);
85
+ }
86
+ return true;
87
+ } catch (error) {
88
+ console.error('Failed to copy text:', error);
89
+ try {
90
+ // Fallback for older browsers
91
+ const textArea = document.createElement('textarea');
92
+ textArea.value = text;
93
+ document.body.appendChild(textArea);
94
+ textArea.select();
95
+ document.execCommand('copy');
96
+ document.body.removeChild(textArea);
97
+ if (itemKey) {
98
+ setCopyStatus(prev => ({ ...prev, [itemKey]: 'success' }));
99
+ setTimeout(() => setCopyStatus(prev => ({ ...prev, [itemKey]: null })), 2000);
100
+ }
101
+ return true;
102
+ } catch (fallbackError) {
103
+ if (itemKey) {
104
+ setCopyStatus(prev => ({ ...prev, [itemKey]: 'error' }));
105
+ setTimeout(() => setCopyStatus(prev => ({ ...prev, [itemKey]: null })), 2000);
106
+ }
107
+ return false;
108
+ }
109
+ }
110
+ }, []);
111
+
112
  const stopAnalysis = useCallback(() => {
113
  abortControllerRef.current?.abort();
114
  setIsAnalyzing(false);
115
  }, []);
116
 
117
+ const copyAllCaptions = useCallback(async () => {
118
+ const captionsText = results
119
+ .filter(result => result.caption && !result.error)
120
+ .map((result, index) => `Image ${index + 1} (${result.file.name}): ${result.caption}`)
121
+ .join('\n\n');
122
+
123
+ if (captionsText) {
124
+ const success = await copyToClipboard(captionsText);
125
+ setCopyAllStatus(success ? 'success' : 'error');
126
+ setTimeout(() => setCopyAllStatus(null), 2000);
127
+ }
128
+ }, [results, copyToClipboard]);
129
+
130
  useEffect(() => {
131
  return () => {
132
  abortControllerRef.current?.abort();
 
171
  )}
172
  </div>
173
 
174
+ {results.length > 0 && !isAnalyzing && (
175
+ <div className="mb-4">
176
+ <GlassButton
177
+ onClick={copyAllCaptions}
178
+ className={`w-full text-sm transition-colors ${
179
+ copyAllStatus === 'success' ? 'bg-green-500/20 border-green-400/30' :
180
+ copyAllStatus === 'error' ? 'bg-red-500/20 border-red-400/30' : ''
181
+ }`}
182
+ disabled={results.filter(r => r.caption && !r.error).length === 0}
183
+ >
184
+ {copyAllStatus === 'success' ? (
185
+ <>
186
+ <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
187
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
188
+ </svg>
189
+ Copied!
190
+ </>
191
+ ) : copyAllStatus === 'error' ? (
192
+ <>
193
+ <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
194
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
195
+ </svg>
196
+ Failed to Copy
197
+ </>
198
+ ) : (
199
+ <>
200
+ <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
201
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
202
+ </svg>
203
+ Copy All Captions
204
+ </>
205
+ )}
206
+ </GlassButton>
207
+ </div>
208
+ )}
209
+
210
  {isAnalyzing && (
211
  <div className="text-sm text-white/70 text-center">
212
  Analyzing image {currentImageIndex + 1} of {images.length}...
 
263
  Error: {result.error}
264
  </div>
265
  ) : (
266
+ <div className="space-y-2">
267
+ <div className="text-white/80">
268
+ {result.caption}
269
+ </div>
270
+ {result.caption && (
271
+ <button
272
+ onClick={(e) => {
273
+ e.stopPropagation();
274
+ const itemKey = `${file.name}-${index}`;
275
+ copyToClipboard(result.caption, itemKey);
276
+ }}
277
+ className={`flex items-center gap-1 transition-colors ${
278
+ copyStatus[`${file.name}-${index}`] === 'success' ? 'text-green-400' :
279
+ copyStatus[`${file.name}-${index}`] === 'error' ? 'text-red-400' :
280
+ 'text-blue-400 hover:text-blue-300'
281
+ }`}
282
+ title="Copy caption"
283
+ >
284
+ {copyStatus[`${file.name}-${index}`] === 'success' ? (
285
+ <>
286
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
287
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
288
+ </svg>
289
+ <span className="text-xs">Copied!</span>
290
+ </>
291
+ ) : copyStatus[`${file.name}-${index}`] === 'error' ? (
292
+ <>
293
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
294
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
295
+ </svg>
296
+ <span className="text-xs">Failed</span>
297
+ </>
298
+ ) : (
299
+ <>
300
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
301
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
302
+ </svg>
303
+ <span className="text-xs">Copy</span>
304
+ </>
305
+ )}
306
+ </button>
307
+ )}
308
  </div>
309
  )}
310
  </div>
src/components/LoadingScreen.tsx CHANGED
@@ -37,11 +37,13 @@ export default function LoadingScreen({ onComplete }: LoadingScreenProps) {
37
  await loadModel((message) => {
38
  setCurrentStep(message);
39
  if (message.includes("Loading processor")) {
40
- setProgress(10);
41
  } else if (message.includes("Processor loaded")) {
42
- setProgress(20);
 
 
43
  } else if (message.includes("Model loaded")) {
44
- setProgress(80);
45
  }
46
  });
47
 
@@ -100,14 +102,21 @@ export default function LoadingScreen({ onComplete }: LoadingScreenProps) {
100
  </div>
101
 
102
  {!isError && (
103
- <div className="space-y-2">
104
- <div className="w-full bg-gray-800/50 rounded-full h-3 overflow-hidden backdrop-blur-sm border border-gray-700/30">
105
  <div
106
- className="h-full bg-gradient-to-r from-blue-500 to-blue-600 rounded-full transition-all duration-300 ease-out"
107
  style={{ width: `${progress}%` }}
108
- />
 
 
 
 
 
 
 
 
109
  </div>
110
- <p className="text-sm text-gray-500">{Math.round(progress)}% complete</p>
111
  </div>
112
  )}
113
 
 
37
  await loadModel((message) => {
38
  setCurrentStep(message);
39
  if (message.includes("Loading processor")) {
40
+ setProgress(15);
41
  } else if (message.includes("Processor loaded")) {
42
+ setProgress(35);
43
+ } else if (message.includes("Loading model")) {
44
+ setProgress(50);
45
  } else if (message.includes("Model loaded")) {
46
+ setProgress(90);
47
  }
48
  });
49
 
 
102
  </div>
103
 
104
  {!isError && (
105
+ <div className="space-y-3">
106
+ <div className="w-full bg-gray-800/50 rounded-full h-4 overflow-hidden backdrop-blur-sm border border-gray-700/30 shadow-inner">
107
  <div
108
+ className="h-full bg-gradient-to-r from-blue-500 via-blue-400 to-cyan-400 rounded-full transition-all duration-500 ease-out relative"
109
  style={{ width: `${progress}%` }}
110
+ >
111
+ <div className="absolute inset-0 bg-white/20 rounded-full animate-pulse" />
112
+ </div>
113
+ </div>
114
+ <div className="flex justify-between items-center text-sm">
115
+ <span className="text-gray-400">{Math.round(progress)}% complete</span>
116
+ {progress > 0 && progress < 100 && (
117
+ <span className="text-blue-400 animate-pulse">●</span>
118
+ )}
119
  </div>
 
120
  </div>
121
  )}
122