mirror of
https://github.com/mito-systems/ranger-app.git
synced 2026-03-01 13:14:07 +00:00
Some checks failed
Publish Ranger template to Laconic Registry / laconic_publish (push) Failing after 1m4s
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
// src/components/ImageAnalysisCard.tsx
|
|
'use client'
|
|
|
|
import React, { useState, useRef } from 'react'
|
|
import { Leaf } from 'lucide-react'
|
|
import { APP_CONFIG } from '../config/appConfig'
|
|
|
|
|
|
interface ImageAnalysisCardProps {
|
|
title: string
|
|
description: string
|
|
onAnalyze: (file: File) => Promise<{ description?: string, error?: string }>
|
|
}
|
|
|
|
interface AnalysisState {
|
|
loading: boolean
|
|
imageUrl: string | null
|
|
description: string | null
|
|
error: string | null
|
|
}
|
|
|
|
const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
|
|
title,
|
|
description,
|
|
onAnalyze
|
|
}) => {
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
const [analysisState, setAnalysisState] = useState<AnalysisState>({
|
|
loading: false,
|
|
imageUrl: null,
|
|
description: null,
|
|
error: null,
|
|
})
|
|
|
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) {
|
|
const imageUrl = URL.createObjectURL(file)
|
|
setAnalysisState({
|
|
...analysisState,
|
|
imageUrl,
|
|
description: null,
|
|
error: null
|
|
})
|
|
}
|
|
}
|
|
|
|
const handleAnalyze = async () => {
|
|
const file = fileInputRef.current?.files?.[0]
|
|
|
|
if (!file) return
|
|
|
|
setAnalysisState({
|
|
...analysisState,
|
|
loading: true,
|
|
error: null,
|
|
})
|
|
|
|
try {
|
|
const result = await onAnalyze(file)
|
|
|
|
if (result.error) {
|
|
setAnalysisState({
|
|
...analysisState,
|
|
loading: false,
|
|
error: result.error,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (result.description) {
|
|
setAnalysisState({
|
|
loading: false,
|
|
imageUrl: analysisState.imageUrl,
|
|
description: result.description,
|
|
error: null,
|
|
})
|
|
} else {
|
|
throw new Error('No analysis received')
|
|
}
|
|
} catch (error) {
|
|
setAnalysisState({
|
|
...analysisState,
|
|
loading: false,
|
|
error: error instanceof Error ? error.message : 'Analysis failed',
|
|
})
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="w-full bg-emerald-900/20 backdrop-blur-lg rounded-2xl shadow-xl border border-emerald-800/50 mb-8 hover:shadow-emerald-500/20 transition-all duration-300">
|
|
<div className="p-6">
|
|
<div className="mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<Leaf className="w-6 h-6 text-emerald-400" />
|
|
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
<p className="text-emerald-200 mt-2">{description}</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Image Upload Area */}
|
|
<div
|
|
className="relative border-2 border-dashed border-emerald-800/50 rounded-xl p-4 text-center
|
|
hover:border-emerald-500/50 transition-colors duration-200
|
|
bg-emerald-950/30"
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleFileSelect}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer
|
|
disabled:cursor-not-allowed"
|
|
/>
|
|
<div className="space-y-2">
|
|
<div className="text-emerald-300">
|
|
{analysisState.imageUrl ? (
|
|
<img
|
|
src={analysisState.imageUrl}
|
|
alt="Selected"
|
|
className="max-h-64 mx-auto rounded-lg"
|
|
/>
|
|
) : (
|
|
<p>Share pictures of {APP_CONFIG.description}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleAnalyze}
|
|
disabled={analysisState.loading || !analysisState.imageUrl}
|
|
className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600
|
|
hover:to-teal-600 text-white font-semibold py-4 px-6 rounded-xl
|
|
transition-all duration-200 shadow-lg hover:shadow-emerald-500/25
|
|
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
|
>
|
|
{analysisState.loading ? 'Processing...' : 'Analyze' }
|
|
</button>
|
|
</div>
|
|
|
|
{analysisState.error && (
|
|
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
|
|
{analysisState.error}
|
|
</div>
|
|
)}
|
|
|
|
{analysisState.description && (
|
|
<div className="mt-4 bg-green-900/30 border border-emerald-800/50 rounded-xl p-4">
|
|
<p className="text-emerald-200 whitespace-pre-wrap">{analysisState.description}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ImageAnalysisCard
|