diff --git a/App.tsx b/App.tsx index 20e0cbd..eb04be1 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,5 @@ + import React, { useState, useRef, useEffect } from 'react'; import ChatView from './views/ChatView'; import CreativeStudio from './views/CreativeStudio'; @@ -10,7 +11,7 @@ import ListeningView from './views/ListeningView'; import ToastContainer, { ToastMessage } from './components/Toast'; import ConfirmModal from './components/ConfirmModal'; import Onboarding from './components/Onboarding'; -import { MessageCircle, Palette, Mic2, Settings, Globe, Sparkles, BookOpen, Languages, Download, Upload, FileText, X, ScanText, Key, Save, Trash2, Menu, BrainCircuit, Link, Headphones } from 'lucide-react'; +import { MessageCircle, Palette, Mic2, Settings, Globe, Sparkles, BookOpen, Languages, Download, Upload, FileText, X, ScanText, Key, Save, Trash2, Menu, BrainCircuit, Link, Headphones, AlertTriangle } from 'lucide-react'; import { AppMode, Language, ChatMessage, TranslationRecord, AppDataBackup, Role, MessageType, ReadingLessonRecord, AVAILABLE_CHAT_MODELS, ChatSession, OCRRecord, ListeningLessonRecord } from './types'; import { translations } from './utils/localization'; import { USER_API_KEY_STORAGE, USER_BASE_URL_STORAGE } from './services/geminiService'; @@ -312,6 +313,35 @@ const App: React.FC = () => { }); }; + const handleClearData = () => { + setConfirmState({ + isOpen: true, + title: t.settings.clearDataTitle, + message: t.settings.clearDataConfirm, + onConfirm: () => { + // 1. Reset State + setChatSessions([]); + setTranslationHistory([]); + setReadingHistory([]); + setListeningHistory([]); + setOcrHistory([]); + + // 2. Clear Storage Explicitly + localStorage.removeItem(STORAGE_KEYS.CHAT_SESSIONS); + localStorage.removeItem(STORAGE_KEYS.TRANSLATION_HISTORY); + localStorage.removeItem(STORAGE_KEYS.READING_HISTORY); + localStorage.removeItem(STORAGE_KEYS.LISTENING_HISTORY); + localStorage.removeItem(STORAGE_KEYS.OCR_HISTORY); + + // 3. Re-init session + createNewSession(); + + addToast('success', t.common.dataCleared); + setConfirmState(prev => ({ ...prev, isOpen: false })); + } + }); + }; + const addToast = (type: 'success' | 'error' | 'info', message: string) => { const id = Date.now().toString(); setToasts(prev => [...prev, { id, type, message }]); @@ -536,6 +566,18 @@ const App: React.FC = () => { + + {/* Danger Zone */} +
+

{t.settings.clearDataTitle}

+
+
+
{t.settings.clearDataDesc}
+ +
+
+
+ @@ -544,4 +586,4 @@ const App: React.FC = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/README.md b/README.md index 098f1f6..50b0df3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ + + # Sakura Sensei 🌸 - AI Japanese Tutor ![React](https://img.shields.io/badge/React-19.0-blue?logo=react) @@ -18,7 +20,7 @@ ### ✨ Features * **Tutor Dojo (Chat):** - * Free chat with Sakura (AI Tutor) using `gemini-3-pro-preview` (Reasoning) or `gemini-2.5-flash`. + * Free chat with Sakura (AI Tutor) using `gemini-3-pro-preview` (Reasoning) or `gemini-3-flash-preview`. * **Voice Interaction:** Real-time Speech-to-Text (STT) and high-quality Text-to-Speech (TTS). * **Thinking Mode:** Visualize the AI's reasoning process for complex grammar explanations. * **Share:** Export chat history as Text, File, or Image (screenshot). @@ -74,9 +76,9 @@ * **Styling:** Tailwind CSS, Lucide React (Icons) * **AI Integration:** `@google/genai` SDK * **Models Used:** - * Text/Reasoning: `gemini-3-pro-preview`, `gemini-2.5-flash` + * Text/Reasoning: `gemini-3-pro-preview`, `gemini-3-flash-preview` * Audio: `gemini-2.5-flash-preview-tts` - * Vision/OCR: `gemini-2.5-flash` + * Vision/OCR: `gemini-3-flash-preview` * Image Gen: `imagen-4.0-generate-001`, `gemini-2.5-flash-image` * Video: `veo-3.1-fast-generate-preview` @@ -189,4 +191,4 @@ MIT License. -Powered by [Google Gemini API](https://ai.google.dev/). +Powered by [Google Gemini API](https://ai.google.dev/). \ No newline at end of file diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index 98590f8..fc2eb8c 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { Component, ErrorInfo, ReactNode } from "react"; +import React, { ErrorInfo, ReactNode } from "react"; import { AlertCircle, RefreshCw, Trash2 } from 'lucide-react'; interface Props { @@ -10,7 +10,7 @@ interface State { error: Error | null; } -export class ErrorBoundary extends Component { +export class ErrorBoundary extends React.Component { public state: State = { hasError: false, error: null diff --git a/releases/HTY1024-APP-SKR-0.8.0_20251223.zip b/releases/HTY1024-APP-SKR-0.8.0_20251223.zip new file mode 100644 index 0000000..0a75c72 Binary files /dev/null and b/releases/HTY1024-APP-SKR-0.8.0_20251223.zip differ diff --git a/services/geminiService.ts b/services/geminiService.ts index 18bec9d..2498fdd 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -132,7 +132,7 @@ class GeminiService { // Ensure model name is clean let modelName = useThinking ? 'gemini-3-pro-preview' - : (imageBase64 ? 'gemini-3-pro-preview' : (modelOverride || 'gemini-2.5-flash')); + : (imageBase64 ? 'gemini-3-pro-preview' : (modelOverride || 'gemini-3-flash-preview')); // Extra safety: strip quotes just in case modelName = modelName.replace(/['"]/g, ''); @@ -278,7 +278,7 @@ class GeminiService { const ai = this.getAi(); return this.retryOperation(async () => { const response = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [ { inlineData: { mimeType: 'audio/wav', data: audioBase64 } }, @@ -368,7 +368,7 @@ class GeminiService { return this.retryOperation(async () => { const response = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ inlineData: { mimeType: 'audio/wav', data: audioBase64 } }, { text: prompt }] }, @@ -401,7 +401,7 @@ class GeminiService { return this.retryOperation(async () => { const response = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ text: prompt }] }, config: { responseMimeType: "application/json", @@ -439,7 +439,7 @@ class GeminiService { return this.retryOperation(async () => { const response = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ text: prompt }] }, config: { responseMimeType: "application/json", @@ -481,7 +481,7 @@ class GeminiService { const prompt = `Tutor for text "${lesson.title}". Question: "${question}". History: ${history}. Explain in ${LANGUAGE_MAP[language]}.`; return this.retryOperation(async () => { const res = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ text: prompt }] } }); return res.text || ""; @@ -492,7 +492,7 @@ class GeminiService { const ai = this.getAi(); return this.retryOperation(async () => { const res = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ text: `Translate the following text from ${source} to ${target}.` }, { text: text }] }, config: { responseMimeType: "application/json", @@ -512,7 +512,7 @@ class GeminiService { const cleanBase64 = base64.replace(/^data:image\/(png|jpeg|jpg|webp|heic|heif);base64,/i, ""); return this.retryOperation(async () => { const res = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ inlineData: { mimeType: 'image/jpeg', data: cleanBase64 } }, { text: `Extract text (Language: ${source}) and translate to ${target}. JSON output: original, translated.` }] }, @@ -537,7 +537,7 @@ class GeminiService { return this.retryOperation(async () => { const res = await ai.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-3-flash-preview', contents: { parts: [{ inlineData: { mimeType: 'image/jpeg', data: cleanBase64 } }, { text: prompt }] }, diff --git a/types.ts b/types.ts index 65a9cef..0e00be2 100644 --- a/types.ts +++ b/types.ts @@ -51,18 +51,18 @@ export type Language = 'en' | 'ja' | 'zh'; // Specific Gemini Models export enum ModelNames { - TEXT_FAST = 'gemini-2.5-flash', + TEXT_FAST = 'gemini-3-flash-preview', TEXT_REASONING = 'gemini-3-pro-preview', TTS = 'gemini-2.5-flash-preview-tts', IMAGE_GEN = 'imagen-4.0-generate-001', IMAGE_EDIT = 'gemini-2.5-flash-image', // Nano Banana VIDEO_GEN = 'veo-3.1-fast-generate-preview', - TRANSCRIPTION = 'gemini-2.5-flash', + TRANSCRIPTION = 'gemini-3-flash-preview', } export const AVAILABLE_CHAT_MODELS = [ { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro (Default - Best Reasoning)' }, - { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash (Fast & Balanced)' } + { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash (Fast & Balanced)' } ]; // Speaking Mode Types diff --git a/utils/localization.ts b/utils/localization.ts index 0db96fc..cfd683b 100644 --- a/utils/localization.ts +++ b/utils/localization.ts @@ -218,7 +218,8 @@ export const translations = { today: "Today", yesterday: "Yesterday", storageFull: "Storage full. History may not be saved.", - storageOptimized: "Storage full. Cleared audio cache to save text." + storageOptimized: "Storage full. Cleared audio cache to save text.", + dataCleared: "All data has been cleared." }, onboarding: { welcome: "Welcome to Sakura Sensei!", @@ -420,7 +421,11 @@ export const translations = { keyRemoved: "Settings cleared.", modelTitle: "AI Model", modelDesc: "Select model for chat/reasoning.", - modelSaved: "Model updated!" + modelSaved: "Model updated!", + clearDataTitle: "Danger Zone", + clearDataBtn: "Clear All Data", + clearDataDesc: "Deletes all chats and history. Configuration is preserved.", + clearDataConfirm: "Are you sure? This will delete ALL chat history, lessons, and scans.\n\n⚠️ WARNING: This action is irreversible.\n👉 TIP: Please BACKUP your data before proceeding!" }, recorder: { start: "Start Mic", @@ -472,7 +477,8 @@ export const translations = { today: "今日", yesterday: "昨日", storageFull: "保存容量がいっぱいです。履歴が保存されない可能性があります。", - storageOptimized: "保存容量がいっぱいのため、音声キャッシュを削除してテキストのみ保存しました。" + storageOptimized: "保存容量がいっぱいのため、音声キャッシュを削除してテキストのみ保存しました。", + dataCleared: "すべてのデータが消去されました。" }, onboarding: { welcome: "さくら先生へようこそ!", @@ -674,7 +680,11 @@ export const translations = { keyRemoved: "設定をクリアしました。", modelTitle: "AIモデル", modelDesc: "チャット/推論用のモデルを選択。", - modelSaved: "モデルを更新しました!" + modelSaved: "モデルを更新しました!", + clearDataTitle: "危険エリア", + clearDataBtn: "すべてのデータを消去", + clearDataDesc: "チャットや履歴をすべて削除します。設定は保持されます。", + clearDataConfirm: "本当に実行しますか?すべてのチャット履歴、レッスン、スキャン記録が削除されます。\n\n⚠️ 警告:この操作は取り消せません。\n👉 ヒント:実行前にバックアップを作成することを強く推奨します!" }, recorder: { start: "マイク開始", @@ -726,7 +736,8 @@ export const translations = { today: "今天", yesterday: "昨天", storageFull: "存储空间已满,历史记录可能无法保存。", - storageOptimized: "存储空间已满,已自动清除音频缓存以保存文本。" + storageOptimized: "存储空间已满,已自动清除音频缓存以保存文本。", + dataCleared: "所有数据已清除。" }, onboarding: { welcome: "欢迎来到樱花老师!", @@ -928,7 +939,11 @@ export const translations = { keyRemoved: "设置已清除。", modelTitle: "AI模型", modelDesc: "选择聊天/推理模型。", - modelSaved: "模型已更新!" + modelSaved: "模型已更新!", + clearDataTitle: "危险区域", + clearDataBtn: "清除所有数据", + clearDataDesc: "删除所有聊天和历史记录。保留设置。", + clearDataConfirm: "您确定吗?这将删除所有聊天记录、课程和扫描记录。\n\n⚠️ 警告:此操作无法撤销。\n👉 提示:请在继续之前先备份您的数据!" }, recorder: { start: "开始录音", diff --git a/views/ListeningView.tsx b/views/ListeningView.tsx index d790308..e545863 100644 --- a/views/ListeningView.tsx +++ b/views/ListeningView.tsx @@ -746,23 +746,23 @@ const ListeningView: React.FC = ({ language, history, onSave )} - - {/* Floating Ask Button */} - {selectedText && ( -
- -
- )} )} + + {/* Floating Ask Button */} + {selectedText && ( +
+ +
+ )} {/* Right: Tutor Chat */} diff --git a/views/OCRView.tsx b/views/OCRView.tsx index 9944cc5..a9e377a 100644 --- a/views/OCRView.tsx +++ b/views/OCRView.tsx @@ -359,7 +359,7 @@ const OCRView: React.FC = ({ language, history, onSaveToHistory, o
{/* LEFT: Main Content (Image, Text, Notes, Vocab) */} -
+
{/* Header */}
@@ -402,108 +402,109 @@ const OCRView: React.FC = ({ language, history, onSaveToHistory, o
{/* Content Scroll Area */} -
- - {/* 1. Image & Extracted Text */} -
-
-
- {imagePreview ? ( - scan result - ) : ( -
- -
- )} -
-
-
-
-

{t.extractedTitle}

-
- - +
+
+ {/* 1. Image & Extracted Text */} +
+
+
+ {imagePreview ? ( + scan result + ) : ( +
+ +
+ )}
-
- {analysis?.extractedText || ''} -
-
-
- - {/* 2. Summary */} -
-

{t.summaryHeader}

-

{analysis?.summary || ''}

-
- - {/* 3. Vocabulary */} -
-

{t.vocabHeader}

-
- {analysis?.vocabulary?.map((v, i) => ( - v ? ( -
-
-
- {v.word || ''} - ({v.reading || ''}) -
+
+
+

{t.extractedTitle}

+
+
- {v.meaning || ''}
- ) : null - ))} +
+ {analysis?.extractedText || ''} +
+
-
- {/* 4. Grammar */} - {analysis?.grammarPoints && analysis.grammarPoints?.length > 0 && ( -
-

{t.grammarHeader}

-
- {analysis.grammarPoints.map((g, i) => ( - g ? ( -
-
{g.point || ''}
-

{g.explanation || ''}

+ {/* 2. Summary */} +
+

{t.summaryHeader}

+

{analysis?.summary || ''}

+
+ + {/* 3. Vocabulary */} +
+

{t.vocabHeader}

+
+ {analysis?.vocabulary?.map((v, i) => ( + v ? ( +
+
+
+ {v.word || ''} + ({v.reading || ''}) +
+ +
+ {v.meaning || ''}
) : null ))}
- )} - {/* Floating Ask Button */} - {selectedText && ( -
- -
- )} + {/* 4. Grammar */} + {analysis?.grammarPoints && analysis.grammarPoints?.length > 0 && ( +
+

{t.grammarHeader}

+
+ {analysis.grammarPoints.map((g, i) => ( + g ? ( +
+
{g.point || ''}
+

{g.explanation || ''}

+
+ ) : null + ))} +
+
+ )} +
+ + {/* Floating Ask Button - Fixed in parent */} + {selectedText && ( +
+ +
+ )}
{/* RIGHT: Tutor Chat (Tab: tutor) */} diff --git a/views/ReadingView.tsx b/views/ReadingView.tsx index bc35067..230d958 100644 --- a/views/ReadingView.tsx +++ b/views/ReadingView.tsx @@ -644,20 +644,20 @@ const ReadingView: React.FC = ({ language, history, onSaveToHi
)}
- - {/* Floating Ask Button */} - {selectedText && ( -
- -
- )}
+ + {/* Floating Ask Button - Fixed Position in Parent */} + {selectedText && ( +
+ +
+ )}
{/* Right: Tutor Chat (Only visible in lesson mode) */}