Spaces:
Running
Running
update user api re-cycling
Browse files
Home.tsx
CHANGED
|
@@ -98,8 +98,8 @@ const cropImage = (
|
|
| 98 |
|
| 99 |
// FIX: Changed to an async function declaration to avoid JSX parsing issues with Promise return types.
|
| 100 |
// FIX: Renamed `history` parameter to `descriptions` to avoid conflict with the browser's built-in `History` type.
|
| 101 |
-
async function serviceDescribeImage(imageDataUrl: string, descriptions: ImageDescription[]): Promise<ImageDescription> {
|
| 102 |
-
const ai = new GoogleGenAI({ apiKey
|
| 103 |
const parts = imageDataUrl.split(',');
|
| 104 |
const mimeType = parts[0].match(/:(.*?);/)?.[1] || 'image/png';
|
| 105 |
const base64Data = parts[1];
|
|
@@ -175,8 +175,8 @@ ${descriptions.length ? descriptions.filter(Boolean).map((desc,index)=>`${index+
|
|
| 175 |
}
|
| 176 |
|
| 177 |
// FIX: Changed to an async function declaration to avoid JSX parsing issues with Promise return types.
|
| 178 |
-
async function serviceEnhance(croppedImageDataUrl: string, history: string[]): Promise<{ imageSrc: string }> {
|
| 179 |
-
const ai = new GoogleGenAI({ apiKey
|
| 180 |
const base64Data = croppedImageDataUrl.split(',')[1] || '';
|
| 181 |
const imagePart = {
|
| 182 |
inlineData: {
|
|
@@ -804,6 +804,10 @@ export default function Home() {
|
|
| 804 |
const containerRef = useRef<HTMLDivElement>(null);
|
| 805 |
const imageObjectURLRef = useRef<string | null>(null);
|
| 806 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
|
| 808 |
const loadInitialImage = useCallback(async () => {
|
| 809 |
if (imageObjectURLRef.current) {
|
|
@@ -928,17 +932,34 @@ export default function Home() {
|
|
| 928 |
|
| 929 |
const handleProcessClick = useCallback(() => {
|
| 930 |
if (!stagedSelection) return;
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 934 |
|
| 935 |
const runEnhancementJob = useCallback(async () => {
|
| 936 |
-
if (!enhancementJob || !image) return;
|
| 937 |
setIsProcessing(true);
|
| 938 |
try {
|
| 939 |
const { originalRect, canvasWithSelectionDataUrl, pixelatedSrc } = enhancementJob;
|
| 940 |
const descriptionHistory = history.slice(0, historyIndex + 1).map(h => h.description).filter((d): d is ImageDescription => d !== null);
|
| 941 |
-
const description = await serviceDescribeImage(canvasWithSelectionDataUrl, descriptionHistory);
|
| 942 |
setNewHistoryEntryData({ description, originalRect });
|
| 943 |
const sourceImageWidth = image.naturalWidth;
|
| 944 |
const sourceImageHeight = image.naturalHeight;
|
|
@@ -957,7 +978,7 @@ export default function Home() {
|
|
| 957 |
const targetHeight = Math.round(targetWidth * aspect);
|
| 958 |
const croppedForEnhancement = await cropImage(image, paddedRect, targetWidth, targetHeight, false);
|
| 959 |
const prompts = [...descriptionHistory.map(d=>(d.prompt || '')), description.prompt || ''];
|
| 960 |
-
const { imageSrc: enhancedPaddedSrc } = await serviceEnhance(croppedForEnhancement, prompts);
|
| 961 |
|
| 962 |
const enhancedPaddedImage = await new Promise<HTMLImageElement>((resolve, reject) => {
|
| 963 |
const img = new Image();
|
|
@@ -988,7 +1009,7 @@ export default function Home() {
|
|
| 988 |
setEnhancementJob(null);
|
| 989 |
setIsProcessing(false);
|
| 990 |
}
|
| 991 |
-
}, [enhancementJob, image, history, historyIndex]);
|
| 992 |
|
| 993 |
const handleEnhancementComplete = useCallback(() => {
|
| 994 |
if (enhancedImageSrc && newHistoryEntryData) {
|
|
@@ -1046,7 +1067,7 @@ export default function Home() {
|
|
| 1046 |
}, [history, historyIndex, appState, isGeneratingGif]);
|
| 1047 |
|
| 1048 |
const handleRegenerate = useCallback(async () => {
|
| 1049 |
-
if (historyIndex <= 0 || appState === AppState.ENHANCING || isGeneratingGif) return;
|
| 1050 |
setAppState(AppState.ENHANCING);
|
| 1051 |
setStagedSelection(null);
|
| 1052 |
const previousStep = history[historyIndex - 1];
|
|
@@ -1058,7 +1079,7 @@ export default function Home() {
|
|
| 1058 |
try {
|
| 1059 |
const descriptionHistory = history.slice(0, historyIndex).map(h => h.description).filter((d): d is ImageDescription => d !== null);
|
| 1060 |
const croppedForDescription = await cropImage(sourceImage, originalRect, originalRect.w, originalRect.h, false);
|
| 1061 |
-
const description = await serviceDescribeImage(croppedForDescription, descriptionHistory);
|
| 1062 |
const sourceImageWidth = sourceImage.naturalWidth;
|
| 1063 |
const sourceImageHeight = sourceImage.naturalHeight;
|
| 1064 |
const padding = 0.5;
|
|
@@ -1076,7 +1097,7 @@ export default function Home() {
|
|
| 1076 |
const targetHeight = Math.round(targetWidth * aspect);
|
| 1077 |
const croppedForEnhancement = await cropImage(sourceImage, paddedRect, targetWidth, targetHeight, false);
|
| 1078 |
const prompts = [...descriptionHistory.map(d=>(d.prompt || '')), description.prompt || ''];
|
| 1079 |
-
const { imageSrc: enhancedPaddedSrc } = await serviceEnhance(croppedForEnhancement, prompts);
|
| 1080 |
|
| 1081 |
const enhancedPaddedImage = await new Promise<HTMLImageElement>((resolve, reject) => {
|
| 1082 |
const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.onerror = reject; img.src = enhancedPaddedSrc;
|
|
@@ -1100,7 +1121,7 @@ export default function Home() {
|
|
| 1100 |
} catch (error) { console.error("Regeneration failed:", error); setAppState(AppState.LOADED); }
|
| 1101 |
};
|
| 1102 |
sourceImage.src = previousStep.imageSrc;
|
| 1103 |
-
}, [history, historyIndex, appState, isGeneratingGif]);
|
| 1104 |
|
| 1105 |
const handleExportGif = useCallback(async () => {
|
| 1106 |
if (historyIndex < 1) return;
|
|
@@ -1169,6 +1190,42 @@ export default function Home() {
|
|
| 1169 |
)}
|
| 1170 |
<input type="file" ref={fileInputRef} onChange={handleFileSelect} style={{ display: 'none' }} accept="image/*" />
|
| 1171 |
<StatusBar state={appState} useFixedSelectionBox={useFixedSelectionBox} isInitialState={history.length <= 1} onUploadClick={handleUploadClick}/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
</div>
|
| 1173 |
);
|
| 1174 |
}
|
|
|
|
| 98 |
|
| 99 |
// FIX: Changed to an async function declaration to avoid JSX parsing issues with Promise return types.
|
| 100 |
// FIX: Renamed `history` parameter to `descriptions` to avoid conflict with the browser's built-in `History` type.
|
| 101 |
+
async function serviceDescribeImage(imageDataUrl: string, descriptions: ImageDescription[], apiKey: string): Promise<ImageDescription> {
|
| 102 |
+
const ai = new GoogleGenAI({ apiKey });
|
| 103 |
const parts = imageDataUrl.split(',');
|
| 104 |
const mimeType = parts[0].match(/:(.*?);/)?.[1] || 'image/png';
|
| 105 |
const base64Data = parts[1];
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
// FIX: Changed to an async function declaration to avoid JSX parsing issues with Promise return types.
|
| 178 |
+
async function serviceEnhance(croppedImageDataUrl: string, history: string[], apiKey: string): Promise<{ imageSrc: string }> {
|
| 179 |
+
const ai = new GoogleGenAI({ apiKey });
|
| 180 |
const base64Data = croppedImageDataUrl.split(',')[1] || '';
|
| 181 |
const imagePart = {
|
| 182 |
inlineData: {
|
|
|
|
| 804 |
const containerRef = useRef<HTMLDivElement>(null);
|
| 805 |
const imageObjectURLRef = useRef<string | null>(null);
|
| 806 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 807 |
+
|
| 808 |
+
// New state for API key management
|
| 809 |
+
const [apiKey, setApiKey] = useState<string | null>(null);
|
| 810 |
+
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
| 811 |
|
| 812 |
const loadInitialImage = useCallback(async () => {
|
| 813 |
if (imageObjectURLRef.current) {
|
|
|
|
| 932 |
|
| 933 |
const handleProcessClick = useCallback(() => {
|
| 934 |
if (!stagedSelection) return;
|
| 935 |
+
if (apiKey) {
|
| 936 |
+
startEnhancementProcess(stagedSelection.originalRect, stagedSelection.screenRect, stagedSelection.canvasDataUrl);
|
| 937 |
+
setStagedSelection(null);
|
| 938 |
+
} else {
|
| 939 |
+
setShowApiKeyModal(true);
|
| 940 |
+
}
|
| 941 |
+
}, [stagedSelection, startEnhancementProcess, apiKey]);
|
| 942 |
+
|
| 943 |
+
const handleApiKeySubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
| 944 |
+
e.preventDefault();
|
| 945 |
+
const newApiKey = e.currentTarget.apiKey.value;
|
| 946 |
+
if (newApiKey) {
|
| 947 |
+
setApiKey(newApiKey);
|
| 948 |
+
setShowApiKeyModal(false);
|
| 949 |
+
if (stagedSelection) {
|
| 950 |
+
startEnhancementProcess(stagedSelection.originalRect, stagedSelection.screenRect, stagedSelection.canvasDataUrl);
|
| 951 |
+
setStagedSelection(null);
|
| 952 |
+
}
|
| 953 |
+
}
|
| 954 |
+
};
|
| 955 |
|
| 956 |
const runEnhancementJob = useCallback(async () => {
|
| 957 |
+
if (!enhancementJob || !image || !apiKey) return;
|
| 958 |
setIsProcessing(true);
|
| 959 |
try {
|
| 960 |
const { originalRect, canvasWithSelectionDataUrl, pixelatedSrc } = enhancementJob;
|
| 961 |
const descriptionHistory = history.slice(0, historyIndex + 1).map(h => h.description).filter((d): d is ImageDescription => d !== null);
|
| 962 |
+
const description = await serviceDescribeImage(canvasWithSelectionDataUrl, descriptionHistory, apiKey);
|
| 963 |
setNewHistoryEntryData({ description, originalRect });
|
| 964 |
const sourceImageWidth = image.naturalWidth;
|
| 965 |
const sourceImageHeight = image.naturalHeight;
|
|
|
|
| 978 |
const targetHeight = Math.round(targetWidth * aspect);
|
| 979 |
const croppedForEnhancement = await cropImage(image, paddedRect, targetWidth, targetHeight, false);
|
| 980 |
const prompts = [...descriptionHistory.map(d=>(d.prompt || '')), description.prompt || ''];
|
| 981 |
+
const { imageSrc: enhancedPaddedSrc } = await serviceEnhance(croppedForEnhancement, prompts, apiKey);
|
| 982 |
|
| 983 |
const enhancedPaddedImage = await new Promise<HTMLImageElement>((resolve, reject) => {
|
| 984 |
const img = new Image();
|
|
|
|
| 1009 |
setEnhancementJob(null);
|
| 1010 |
setIsProcessing(false);
|
| 1011 |
}
|
| 1012 |
+
}, [enhancementJob, image, history, historyIndex, apiKey]);
|
| 1013 |
|
| 1014 |
const handleEnhancementComplete = useCallback(() => {
|
| 1015 |
if (enhancedImageSrc && newHistoryEntryData) {
|
|
|
|
| 1067 |
}, [history, historyIndex, appState, isGeneratingGif]);
|
| 1068 |
|
| 1069 |
const handleRegenerate = useCallback(async () => {
|
| 1070 |
+
if (historyIndex <= 0 || appState === AppState.ENHANCING || isGeneratingGif || !apiKey) return;
|
| 1071 |
setAppState(AppState.ENHANCING);
|
| 1072 |
setStagedSelection(null);
|
| 1073 |
const previousStep = history[historyIndex - 1];
|
|
|
|
| 1079 |
try {
|
| 1080 |
const descriptionHistory = history.slice(0, historyIndex).map(h => h.description).filter((d): d is ImageDescription => d !== null);
|
| 1081 |
const croppedForDescription = await cropImage(sourceImage, originalRect, originalRect.w, originalRect.h, false);
|
| 1082 |
+
const description = await serviceDescribeImage(croppedForDescription, descriptionHistory, apiKey);
|
| 1083 |
const sourceImageWidth = sourceImage.naturalWidth;
|
| 1084 |
const sourceImageHeight = sourceImage.naturalHeight;
|
| 1085 |
const padding = 0.5;
|
|
|
|
| 1097 |
const targetHeight = Math.round(targetWidth * aspect);
|
| 1098 |
const croppedForEnhancement = await cropImage(sourceImage, paddedRect, targetWidth, targetHeight, false);
|
| 1099 |
const prompts = [...descriptionHistory.map(d=>(d.prompt || '')), description.prompt || ''];
|
| 1100 |
+
const { imageSrc: enhancedPaddedSrc } = await serviceEnhance(croppedForEnhancement, prompts, apiKey);
|
| 1101 |
|
| 1102 |
const enhancedPaddedImage = await new Promise<HTMLImageElement>((resolve, reject) => {
|
| 1103 |
const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.onerror = reject; img.src = enhancedPaddedSrc;
|
|
|
|
| 1121 |
} catch (error) { console.error("Regeneration failed:", error); setAppState(AppState.LOADED); }
|
| 1122 |
};
|
| 1123 |
sourceImage.src = previousStep.imageSrc;
|
| 1124 |
+
}, [history, historyIndex, appState, isGeneratingGif, apiKey]);
|
| 1125 |
|
| 1126 |
const handleExportGif = useCallback(async () => {
|
| 1127 |
if (historyIndex < 1) return;
|
|
|
|
| 1190 |
)}
|
| 1191 |
<input type="file" ref={fileInputRef} onChange={handleFileSelect} style={{ display: 'none' }} accept="image/*" />
|
| 1192 |
<StatusBar state={appState} useFixedSelectionBox={useFixedSelectionBox} isInitialState={history.length <= 1} onUploadClick={handleUploadClick}/>
|
| 1193 |
+
|
| 1194 |
+
{/* API Key Modal */}
|
| 1195 |
+
{showApiKeyModal && (
|
| 1196 |
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
| 1197 |
+
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
| 1198 |
+
<div className="flex justify-between items-start mb-4">
|
| 1199 |
+
<h3 className="text-xl font-bold text-gray-700">
|
| 1200 |
+
Add Gemini API Key
|
| 1201 |
+
</h3>
|
| 1202 |
+
<button
|
| 1203 |
+
onClick={() => setShowApiKeyModal(false)}
|
| 1204 |
+
className="text-gray-400 hover:text-gray-500">
|
| 1205 |
+
<span className="text-2xl">×</span>
|
| 1206 |
+
</button>
|
| 1207 |
+
</div>
|
| 1208 |
+
<p className="text-gray-600 mb-4">
|
| 1209 |
+
Add the API key to process the request. The API key will be
|
| 1210 |
+
removed if the app page is refreshed or closed.
|
| 1211 |
+
</p>
|
| 1212 |
+
<form onSubmit={handleApiKeySubmit}>
|
| 1213 |
+
<input
|
| 1214 |
+
type="password"
|
| 1215 |
+
name="apiKey"
|
| 1216 |
+
className="w-full p-2 border-2 border-gray-300 rounded-md mb-4 text-black"
|
| 1217 |
+
placeholder="Enter your Gemini API Key"
|
| 1218 |
+
required
|
| 1219 |
+
/>
|
| 1220 |
+
<button
|
| 1221 |
+
type="submit"
|
| 1222 |
+
className="w-full bg-black text-white p-2 rounded-md hover:bg-gray-800 transition-colors">
|
| 1223 |
+
Submit
|
| 1224 |
+
</button>
|
| 1225 |
+
</form>
|
| 1226 |
+
</div>
|
| 1227 |
+
</div>
|
| 1228 |
+
)}
|
| 1229 |
</div>
|
| 1230 |
);
|
| 1231 |
}
|