更新至0.5.0_20251123版本
This commit is contained in:
144
App.tsx
144
App.tsx
@@ -27,31 +27,44 @@ const STORAGE_KEYS = {
|
||||
HAS_SEEN_ONBOARDING: 'sakura_has_seen_onboarding'
|
||||
};
|
||||
|
||||
// Robust helper for safe JSON parsing
|
||||
const safeJSONParse = <T,>(key: string, fallback: T): T => {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
if (!item) return fallback;
|
||||
const parsed = JSON.parse(item);
|
||||
|
||||
// Validate Array type consistency if fallback is an array
|
||||
if (Array.isArray(fallback)) {
|
||||
return Array.isArray(parsed) ? (parsed as unknown as T) : fallback;
|
||||
}
|
||||
return parsed as T;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load ${key}, falling back to default.`, e);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [currentView, setCurrentView] = useState<AppMode>(AppMode.CHAT);
|
||||
// Default to 'zh' (Chinese)
|
||||
const [language, setLanguage] = useState<Language>(() => (localStorage.getItem(STORAGE_KEYS.LANGUAGE) as Language) || 'zh');
|
||||
const [chatSessions, setChatSessions] = useState<ChatSession[]>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.CHAT_SESSIONS);
|
||||
if (stored) return JSON.parse(stored);
|
||||
} catch (e) { console.error("Failed to load chat sessions", e); }
|
||||
return [];
|
||||
|
||||
// Safe Language Initialization
|
||||
const [language, setLanguage] = useState<Language>(() => {
|
||||
// Cast to string to prevent TS from inferring the literal type 'zh' from fallback
|
||||
const lang = safeJSONParse(STORAGE_KEYS.LANGUAGE, 'zh') as string;
|
||||
if (lang === 'en' || lang === 'ja' || lang === 'zh') return lang as Language;
|
||||
return 'zh';
|
||||
});
|
||||
const [activeSessionId, setActiveSessionId] = useState<string>(() => localStorage.getItem(STORAGE_KEYS.ACTIVE_SESSION) || '');
|
||||
const [translationHistory, setTranslationHistory] = useState<TranslationRecord[]>(() => {
|
||||
const s = localStorage.getItem(STORAGE_KEYS.TRANSLATION_HISTORY); return s ? JSON.parse(s) : [];
|
||||
});
|
||||
const [readingHistory, setReadingHistory] = useState<ReadingLessonRecord[]>(() => {
|
||||
const s = localStorage.getItem(STORAGE_KEYS.READING_HISTORY); return s ? JSON.parse(s) : [];
|
||||
});
|
||||
const [listeningHistory, setListeningHistory] = useState<ListeningLessonRecord[]>(() => {
|
||||
const s = localStorage.getItem(STORAGE_KEYS.LISTENING_HISTORY); return s ? JSON.parse(s) : [];
|
||||
});
|
||||
const [ocrHistory, setOcrHistory] = useState<OCRRecord[]>(() => {
|
||||
const s = localStorage.getItem(STORAGE_KEYS.OCR_HISTORY); return s ? JSON.parse(s) : [];
|
||||
});
|
||||
const [selectedModel, setSelectedModel] = useState<string>(() => localStorage.getItem(STORAGE_KEYS.SELECTED_MODEL) || AVAILABLE_CHAT_MODELS[0].id);
|
||||
|
||||
// Safe History Initialization
|
||||
const [chatSessions, setChatSessions] = useState<ChatSession[]>(() => safeJSONParse(STORAGE_KEYS.CHAT_SESSIONS, []));
|
||||
const [activeSessionId, setActiveSessionId] = useState<string>(() => safeJSONParse(STORAGE_KEYS.ACTIVE_SESSION, ''));
|
||||
const [translationHistory, setTranslationHistory] = useState<TranslationRecord[]>(() => safeJSONParse(STORAGE_KEYS.TRANSLATION_HISTORY, []));
|
||||
const [readingHistory, setReadingHistory] = useState<ReadingLessonRecord[]>(() => safeJSONParse(STORAGE_KEYS.READING_HISTORY, []));
|
||||
const [listeningHistory, setListeningHistory] = useState<ListeningLessonRecord[]>(() => safeJSONParse(STORAGE_KEYS.LISTENING_HISTORY, []));
|
||||
const [ocrHistory, setOcrHistory] = useState<OCRRecord[]>(() => safeJSONParse(STORAGE_KEYS.OCR_HISTORY, []));
|
||||
|
||||
const [selectedModel, setSelectedModel] = useState<string>(() => safeJSONParse(STORAGE_KEYS.SELECTED_MODEL, AVAILABLE_CHAT_MODELS[0].id));
|
||||
const [hasSeenOnboarding, setHasSeenOnboarding] = useState(() => !!localStorage.getItem(STORAGE_KEYS.HAS_SEEN_ONBOARDING));
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
@@ -65,7 +78,8 @@ const App: React.FC = () => {
|
||||
isOpen: false, title: '', message: '', onConfirm: () => {}
|
||||
});
|
||||
|
||||
const t = translations[language];
|
||||
// Ensure 't' is always valid even if language switch glitches
|
||||
const t = translations[language] || translations['zh'];
|
||||
|
||||
// Safe Storage Helper to prevent white screen on QuotaExceededError
|
||||
const saveToStorage = (key: string, data: any) => {
|
||||
@@ -73,46 +87,31 @@ const App: React.FC = () => {
|
||||
const json = JSON.stringify(data);
|
||||
localStorage.setItem(key, json);
|
||||
} catch (e: any) {
|
||||
// Check for QuotaExceededError
|
||||
if (e.name === 'QuotaExceededError' || e.message?.includes('exceeded the quota')) {
|
||||
console.warn(`Storage quota exceeded for key: ${key}`);
|
||||
addToast('error', translations[language].common.storageFull);
|
||||
addToast('error', t.common.storageFull);
|
||||
|
||||
// Attempt to recover by stripping heavy data (e.g., audioUrl from chats, base64 images from OCR)
|
||||
// Strategy: Try to strip audioUrl/imagePreview from bulky items
|
||||
if (key === STORAGE_KEYS.CHAT_SESSIONS && Array.isArray(data)) {
|
||||
try {
|
||||
// Create a lightweight version by removing audioUrl base64 strings
|
||||
const leanData = (data as ChatSession[]).map(s => ({
|
||||
...s,
|
||||
messages: s.messages.map(m => ({
|
||||
...m,
|
||||
metadata: {
|
||||
...m.metadata,
|
||||
audioUrl: undefined // Remove cached audio to save space
|
||||
}
|
||||
metadata: { ...m.metadata, audioUrl: undefined } // Remove audio cache
|
||||
}))
|
||||
}));
|
||||
localStorage.setItem(key, JSON.stringify(leanData));
|
||||
addToast('info', translations[language].common.storageOptimized);
|
||||
} catch (retryError) {
|
||||
console.error("Failed to save even lean chat data", retryError);
|
||||
}
|
||||
addToast('info', t.common.storageOptimized);
|
||||
} catch (err) {}
|
||||
} else if (key === STORAGE_KEYS.OCR_HISTORY && Array.isArray(data)) {
|
||||
try {
|
||||
// Create lightweight version by removing imagePreview
|
||||
const leanData = (data as OCRRecord[]).map(r => ({
|
||||
...r,
|
||||
imagePreview: '' // Remove heavy base64 image
|
||||
}));
|
||||
const leanData = (data as OCRRecord[]).map(r => ({ ...r, imagePreview: '' })); // Remove image
|
||||
localStorage.setItem(key, JSON.stringify(leanData));
|
||||
addToast('info', translations[language].common.storageOptimized);
|
||||
} catch (retryError) {
|
||||
console.error("Failed to save even lean OCR data", retryError);
|
||||
addToast('info', t.common.storageOptimized);
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error("LocalStorage save failed", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,12 +125,15 @@ const App: React.FC = () => {
|
||||
useEffect(() => { saveToStorage(STORAGE_KEYS.SELECTED_MODEL, selectedModel); }, [selectedModel]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only update welcome message if session is empty/default
|
||||
if (activeSessionId) {
|
||||
const activeSession = chatSessions.find(s => s.id === activeSessionId);
|
||||
if (activeSession && activeSession.messages.length === 1 && activeSession.messages[0].role === Role.MODEL) {
|
||||
const newWelcome = translations[language].chat.welcome;
|
||||
const newTitle = translations[language].chat.newChat;
|
||||
const newWelcome = t.chat.welcome;
|
||||
const newTitle = t.chat.newChat;
|
||||
setChatSessions(prev => prev.map(s => s.id === activeSessionId ? { ...s, title: newTitle, messages: [{ ...s.messages[0], content: newWelcome }] } : s));
|
||||
}
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
const hasInitialized = useRef(false);
|
||||
@@ -149,7 +151,7 @@ const App: React.FC = () => {
|
||||
|
||||
if (!storedKey && (!envKey || envKey.length === 0)) {
|
||||
setIsSettingsOpen(true);
|
||||
setTimeout(() => addToast('info', translations[language].settings.apiKeyMissing), 500);
|
||||
setTimeout(() => addToast('info', t.settings.apiKeyMissing), 500);
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
}
|
||||
@@ -157,8 +159,8 @@ const App: React.FC = () => {
|
||||
|
||||
const createNewSession = () => {
|
||||
const newId = Date.now().toString();
|
||||
const welcomeMsg: ChatMessage = { id: 'welcome', role: Role.MODEL, type: MessageType.TEXT, content: translations[language].chat.welcome, timestamp: Date.now() };
|
||||
setChatSessions(prev => [{ id: newId, title: translations[language].chat.newChat, messages: [welcomeMsg], createdAt: Date.now(), updatedAt: Date.now() }, ...prev]);
|
||||
const welcomeMsg: ChatMessage = { id: 'welcome', role: Role.MODEL, type: MessageType.TEXT, content: t.chat.welcome, timestamp: Date.now() };
|
||||
setChatSessions(prev => [{ id: newId, title: t.chat.newChat, messages: [welcomeMsg], createdAt: Date.now(), updatedAt: Date.now() }, ...prev]);
|
||||
setActiveSessionId(newId);
|
||||
};
|
||||
|
||||
@@ -187,8 +189,8 @@ const App: React.FC = () => {
|
||||
const deleteSession = (sessionId: string) => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].chat.deleteConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.chat.deleteConfirm,
|
||||
onConfirm: () => {
|
||||
const remaining = chatSessions.filter(s => s.id !== sessionId);
|
||||
setChatSessions(remaining);
|
||||
@@ -204,8 +206,8 @@ const App: React.FC = () => {
|
||||
const clearAllChatSessions = () => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.clearHistoryConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.clearHistoryConfirm,
|
||||
onConfirm: () => {
|
||||
setChatSessions([]);
|
||||
createNewSession();
|
||||
@@ -217,8 +219,8 @@ const App: React.FC = () => {
|
||||
const deleteReadingLesson = (id: string) => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.deleteItemConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.deleteItemConfirm,
|
||||
onConfirm: () => {
|
||||
setReadingHistory(prev => prev.filter(item => item.id !== id));
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -229,8 +231,8 @@ const App: React.FC = () => {
|
||||
const clearReadingHistory = () => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.clearHistoryConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.clearHistoryConfirm,
|
||||
onConfirm: () => {
|
||||
setReadingHistory([]);
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -241,8 +243,8 @@ const App: React.FC = () => {
|
||||
const deleteListeningLesson = (id: string) => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.deleteItemConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.deleteItemConfirm,
|
||||
onConfirm: () => {
|
||||
setListeningHistory(prev => prev.filter(item => item.id !== id));
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -253,8 +255,8 @@ const App: React.FC = () => {
|
||||
const clearListeningHistory = () => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.clearHistoryConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.clearHistoryConfirm,
|
||||
onConfirm: () => {
|
||||
setListeningHistory([]);
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -265,8 +267,8 @@ const App: React.FC = () => {
|
||||
const deleteOCRRecord = (id: string) => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.deleteItemConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.deleteItemConfirm,
|
||||
onConfirm: () => {
|
||||
setOcrHistory(prev => prev.filter(item => item.id !== id));
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -277,8 +279,8 @@ const App: React.FC = () => {
|
||||
const clearOCRHistory = () => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.clearHistoryConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.clearHistoryConfirm,
|
||||
onConfirm: () => {
|
||||
setOcrHistory([]);
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -289,8 +291,8 @@ const App: React.FC = () => {
|
||||
const deleteTranslationRecord = (id: string) => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.deleteItemConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.deleteItemConfirm,
|
||||
onConfirm: () => {
|
||||
setTranslationHistory(prev => prev.filter(item => item.id !== id));
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
@@ -301,8 +303,8 @@ const App: React.FC = () => {
|
||||
const clearTranslationHistory = () => {
|
||||
setConfirmState({
|
||||
isOpen: true,
|
||||
title: translations[language].common.confirm,
|
||||
message: translations[language].common.clearHistoryConfirm,
|
||||
title: t.common.confirm,
|
||||
message: t.common.clearHistoryConfirm,
|
||||
onConfirm: () => {
|
||||
setTranslationHistory([]);
|
||||
setConfirmState(prev => ({ ...prev, isOpen: false }));
|
||||
|
||||
70
components/ErrorBoundary.tsx
Normal file
70
components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from "react";
|
||||
import { AlertCircle, RefreshCw, Trash2 } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("Uncaught error:", error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-50 p-6 text-center animate-fade-in">
|
||||
<div className="bg-white p-8 rounded-3xl shadow-xl max-w-md w-full border border-slate-100 flex flex-col items-center">
|
||||
<div className="w-20 h-20 bg-red-50 text-red-500 rounded-full flex items-center justify-center mb-6 shadow-sm">
|
||||
<AlertCircle size={40} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-extrabold text-slate-800 mb-2">Something went wrong</h2>
|
||||
<p className="text-slate-500 mb-8 text-sm leading-relaxed bg-slate-50 p-4 rounded-xl border border-slate-100 w-full font-mono break-all">
|
||||
{this.state.error?.message || "An unexpected error occurred."}
|
||||
</p>
|
||||
|
||||
<div className="space-y-3 w-full">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full py-3.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 transition-all active:scale-95 shadow-lg shadow-indigo-200"
|
||||
>
|
||||
<RefreshCw size={18} /> Reload App
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
className="w-full py-3.5 bg-white border-2 border-red-100 text-red-500 hover:bg-red-50 hover:border-red-200 rounded-xl font-bold flex items-center justify-center gap-2 transition-all active:scale-95"
|
||||
>
|
||||
<Trash2 size={18} /> Clear Data & Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-slate-400 mt-6 font-medium">
|
||||
* If reloading doesn't work, try clearing data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
@@ -10,7 +12,9 @@ if (!rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
BIN
releases/HTY1024-APP-SKR-0.5.0_20251123.zip
Normal file
BIN
releases/HTY1024-APP-SKR-0.5.0_20251123.zip
Normal file
Binary file not shown.
@@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
import { GoogleGenAI, Modality, Type } from "@google/genai";
|
||||
import { PronunciationFeedback, Language, ReadingLesson, ReadingDifficulty, OCRAnalysis, ListeningLesson } from "../types";
|
||||
import { base64ToUint8Array, uint8ArrayToBase64 } from "../utils/audioUtils";
|
||||
@@ -63,7 +62,7 @@ const LANGUAGE_MAP = {
|
||||
class GeminiService {
|
||||
private getAi() {
|
||||
const userKey = localStorage.getItem(USER_API_KEY_STORAGE);
|
||||
const userBaseUrl = localStorage.getItem(USER_BASE_URL_STORAGE);
|
||||
let userBaseUrl = localStorage.getItem(USER_BASE_URL_STORAGE);
|
||||
const envKey = process.env.API_KEY;
|
||||
const keyToUse = (userKey && userKey.trim().length > 0) ? userKey : envKey;
|
||||
|
||||
@@ -73,8 +72,11 @@ class GeminiService {
|
||||
}
|
||||
|
||||
const config: any = { apiKey: keyToUse };
|
||||
|
||||
if (userBaseUrl && userBaseUrl.trim().length > 0) {
|
||||
config.baseUrl = userBaseUrl.trim();
|
||||
// Sanitize Base URL: remove quotes and trailing slashes
|
||||
let cleanUrl = userBaseUrl.trim().replace(/['"]/g, '').replace(/\/+$/, '');
|
||||
config.baseUrl = cleanUrl;
|
||||
}
|
||||
|
||||
return new GoogleGenAI(config);
|
||||
@@ -92,11 +94,19 @@ class GeminiService {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.message || '';
|
||||
|
||||
// Check for Network/CORS/Proxy errors specifically
|
||||
if (errorMsg.includes('Failed to fetch') || errorMsg.includes('NetworkError')) {
|
||||
console.error("Network Error Detected:", error);
|
||||
throw new Error("Network connection failed. Please check your Base URL (Proxy) settings or internet connection.");
|
||||
}
|
||||
|
||||
const isOverloaded =
|
||||
error?.status === 503 ||
|
||||
error?.response?.status === 503 ||
|
||||
error?.message?.includes('503') ||
|
||||
error?.message?.includes('overloaded');
|
||||
errorMsg.includes('503') ||
|
||||
errorMsg.includes('overloaded');
|
||||
|
||||
if (isOverloaded && retries > 0) {
|
||||
console.warn(`Model overloaded (503). Retrying...`);
|
||||
@@ -118,10 +128,14 @@ class GeminiService {
|
||||
): Promise<{ text: string, model: string }> {
|
||||
const ai = this.getAi();
|
||||
|
||||
// Ensure model name is clean
|
||||
let modelName = useThinking
|
||||
? 'gemini-3-pro-preview'
|
||||
: (imageBase64 ? 'gemini-3-pro-preview' : (modelOverride || 'gemini-2.5-flash'));
|
||||
|
||||
// Extra safety: strip quotes just in case
|
||||
modelName = modelName.replace(/['"]/g, '');
|
||||
|
||||
const targetLangName = LANGUAGE_MAP[language];
|
||||
const parts: any[] = [];
|
||||
|
||||
@@ -191,7 +205,7 @@ class GeminiService {
|
||||
return response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data || null;
|
||||
} catch (e) {
|
||||
console.error("TTS Chunk Error", e);
|
||||
return null;
|
||||
throw e; // Throw to retryOperation to handle network errors
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -203,7 +217,11 @@ class GeminiService {
|
||||
|
||||
// If text is short, process directly
|
||||
if (text.length <= MAX_CHUNK_LENGTH) {
|
||||
return this._generateSpeechChunk(text);
|
||||
try {
|
||||
return await this._generateSpeechChunk(text);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Split text into chunks by sentence to avoid breaking words
|
||||
@@ -284,7 +302,7 @@ class GeminiService {
|
||||
return bytes ? `data:image/jpeg;base64,${bytes}` : null;
|
||||
} catch (e) {
|
||||
console.error("Image Gen Error", e);
|
||||
return null;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -310,7 +328,7 @@ class GeminiService {
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error("Image Edit Error", e);
|
||||
return null;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user