更新至 v0.11.0_20251229 版本
This commit is contained in:
14
App.tsx
14
App.tsx
@@ -13,7 +13,7 @@ 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, AlertTriangle, Home } from 'lucide-react';
|
||||
import { AppMode, Language, ChatMessage, TranslationRecord, AppDataBackup, Role, MessageType, ReadingLessonRecord, AVAILABLE_CHAT_MODELS, ChatSession, OCRRecord, ListeningLessonRecord } from './types';
|
||||
import { AppMode, Language, ChatMessage, TranslationRecord, AppDataBackup, Role, MessageType, ReadingLessonRecord, ChatSession, OCRRecord, ListeningLessonRecord } from './types';
|
||||
import { translations } from './utils/localization';
|
||||
import { USER_API_KEY_STORAGE, USER_BASE_URL_STORAGE } from './services/geminiService';
|
||||
|
||||
@@ -25,7 +25,6 @@ const STORAGE_KEYS = {
|
||||
LISTENING_HISTORY: 'sakura_listening_history',
|
||||
OCR_HISTORY: 'sakura_ocr_history',
|
||||
LANGUAGE: 'sakura_language',
|
||||
SELECTED_MODEL: 'sakura_selected_model',
|
||||
HAS_SEEN_ONBOARDING: 'sakura_has_seen_onboarding'
|
||||
};
|
||||
|
||||
@@ -66,7 +65,6 @@ const App: React.FC = () => {
|
||||
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);
|
||||
@@ -124,7 +122,6 @@ const App: React.FC = () => {
|
||||
useEffect(() => { saveToStorage(STORAGE_KEYS.LISTENING_HISTORY, listeningHistory); }, [listeningHistory]);
|
||||
useEffect(() => { saveToStorage(STORAGE_KEYS.OCR_HISTORY, ocrHistory); }, [ocrHistory]);
|
||||
useEffect(() => { saveToStorage(STORAGE_KEYS.LANGUAGE, language); }, [language]);
|
||||
useEffect(() => { saveToStorage(STORAGE_KEYS.SELECTED_MODEL, selectedModel); }, [selectedModel]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only update welcome message if session is empty/default
|
||||
@@ -509,7 +506,7 @@ const App: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{currentView === AppMode.HOME && <HomeView language={language} onNavigate={handleViewChange} />}
|
||||
{currentView === AppMode.CHAT && <ChatView language={language} sessions={chatSessions} activeSessionId={activeSessionId} onNewSession={createNewSession} onSelectSession={setActiveSessionId} onDeleteSession={deleteSession} onClearAllSessions={clearAllChatSessions} onUpdateSession={updateSessionMessages} selectedModel={selectedModel} addToast={addToast} />}
|
||||
{currentView === AppMode.CHAT && <ChatView language={language} sessions={chatSessions} activeSessionId={activeSessionId} onNewSession={createNewSession} onSelectSession={setActiveSessionId} onDeleteSession={deleteSession} onClearAllSessions={clearAllChatSessions} onUpdateSession={updateSessionMessages} addToast={addToast} />}
|
||||
{currentView === AppMode.TRANSLATION && <TranslationView language={language} history={translationHistory} addToHistory={(rec) => setTranslationHistory(prev => [...prev, rec])} clearHistory={clearTranslationHistory} onDeleteHistoryItem={deleteTranslationRecord} />}
|
||||
{currentView === AppMode.SPEAKING && <SpeakingPracticeView language={language} />}
|
||||
{currentView === AppMode.CREATIVE && <CreativeStudio language={language} addToast={addToast} />}
|
||||
@@ -542,12 +539,7 @@ const App: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-400 uppercase mb-4 flex gap-2"><BrainCircuit size={16} /> {t.settings.modelTitle}</h4>
|
||||
<select value={selectedModel} onChange={(e) => { setSelectedModel(e.target.value); addToast('success', t.settings.modelSaved); }} className="w-full p-3 rounded-xl bg-slate-50 border border-slate-200 font-bold text-slate-700 outline-none focus:ring-2 focus:ring-indigo-500 transition-all cursor-pointer hover:bg-white">
|
||||
{AVAILABLE_CHAT_MODELS.map(model => <option key={model.id} value={model.id}>{model.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-400 uppercase mb-4 flex gap-2"><Download size={16} /> {t.settings.backupTitle}</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
|
||||
182
README.md
182
README.md
@@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
# Sakura Sensei 🌸 - AI Japanese Tutor
|
||||
|
||||

|
||||
@@ -8,7 +7,7 @@
|
||||

|
||||

|
||||
|
||||
**Sakura Sensei** is an immersive, all-in-one Japanese learning platform powered by Google's latest Gemini models. It provides a personalized tutor, realistic roleplay scenarios, custom reading/listening materials, and creative tools to make learning Japanese engaging and effective.
|
||||
**Sakura Sensei** is a next-generation language learning platform powered by Google's state-of-the-art **Gemini 3** and **Veo** models. It transcends traditional learning apps by offering a context-aware AI tutor, real-time pronunciation scoring, and generative media tools to create a truly immersive Japanese learning environment.
|
||||
|
||||
[English](#english) | [日本語](#japanese) | [中文](#chinese)
|
||||
|
||||
@@ -17,32 +16,33 @@
|
||||
<a name="english"></a>
|
||||
## 🇬🇧 English
|
||||
|
||||
### ✨ Features
|
||||
### ✨ Key Features
|
||||
|
||||
* **Tutor Dojo (Chat):**
|
||||
* 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).
|
||||
* **Reading Hall:**
|
||||
* Generates custom reading lessons based on your topic and JLPT level (Beginner N5 - Advanced N1).
|
||||
* Includes vocabulary lists, grammar analysis, and translations.
|
||||
* **Contextual Tutor:** Ask questions specifically about the generated text.
|
||||
* **Listening Lab:**
|
||||
* AI-generated conversations and monologues with comprehension quizzes.
|
||||
* Audio playback with speed controls and transcript toggles.
|
||||
* **Roleplay (Speaking):**
|
||||
* Practice realistic scenarios (Cafe, Hotel, Immigration, etc.).
|
||||
* **AI Feedback:** Receive instant scoring on pronunciation, fluency, and grammar corrections.
|
||||
* **Creative Atelier:**
|
||||
* **Paint:** Generate images using `imagen-4.0` to visualize vocabulary.
|
||||
* **Dream Video:** Generate short videos using `veo-3.1` (requires specific API access).
|
||||
* **Toolbox:**
|
||||
* **Scanner (OCR):** Upload or snap photos of Japanese text for instant analysis and study notes.
|
||||
* **Translator:** Text and Image translation with audio support.
|
||||
* **Data Management:**
|
||||
* Local storage for history (Chat, Reading, Listening, OCR).
|
||||
* Backup and Restore functionality (JSON).
|
||||
#### 🗣️ Tutor Dojo (Context-Aware Chat)
|
||||
* **Dual-Model Intelligence:** Switch instantly between **Gemini 3 Flash** (Fast & Responsive) and **Gemini 3 Pro** (Deep Reasoning) directly within the chat interface.
|
||||
* **Deep Context:** The AI remembers your conversation history, allowing for natural, flowing dialogue and follow-up corrections.
|
||||
* **Thinking Mode:** Visualize the AI's "thought process" as it breaks down complex grammar rules or cultural nuances before answering.
|
||||
* **Multimodal Input:** Chat via text, voice (speech-to-text), or by uploading images for analysis.
|
||||
|
||||
#### 🎭 Roleplay (Speaking Practice)
|
||||
* **Real-world Scenarios:** Practice checking into a hotel, ordering at a konbini, or passing immigration.
|
||||
* **AI Audio Feedback:** Receive instant, actionable feedback on your pronunciation, intonation, and grammar.
|
||||
* **Native TTS:** High-fidelity Japanese Text-to-Speech powered by Gemini.
|
||||
|
||||
#### 📜 Reading Hall & 🎧 Listening Lab
|
||||
* **Custom Lesson Generation:** Generate unique reading materials and listening scripts tailored exactly to your JLPT level (N5–N1) and interests.
|
||||
* **Interactive Study:** Click to translate, hear pronunciations, or ask the tutor specific questions about the generated content.
|
||||
* **Comprehension Quizzes:** Test your understanding with AI-generated quizzes.
|
||||
|
||||
#### 🎨 Creative Atelier
|
||||
* **Visual Learning:** Generate images using **Imagen 3** to visualize vocabulary.
|
||||
* **Video Immersion:** Create short, AI-generated videos using **Veo** to see cultural concepts in motion.
|
||||
|
||||
#### 🧰 Toolbox
|
||||
* **OCR Scanner:** Snap a photo of a textbook or menu; the AI extracts the text, translates it, and explains the grammar.
|
||||
* **Smart Translator:** Context-aware translation for text and images.
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Getting Started
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
|
||||
3. **Set up API Key:**
|
||||
* Get your API key from [Google AI Studio](https://aistudio.google.com/).
|
||||
* Create a `.env` file in the root directory:
|
||||
* **Option A (Recommended):** Create a `.env` file in the root directory:
|
||||
```env
|
||||
VITE_API_KEY=your_gemini_api_key_here
|
||||
```
|
||||
* *Alternatively, you can enter the API Key directly in the app's Settings menu.*
|
||||
* **Option B:** Enter the key directly in the app's **Settings** menu.
|
||||
|
||||
4. **Run the app:**
|
||||
```bash
|
||||
@@ -72,118 +72,62 @@
|
||||
|
||||
### 🛠 Tech Stack
|
||||
|
||||
* **Frontend:** React 19, TypeScript, Vite
|
||||
* **Styling:** Tailwind CSS, Lucide React (Icons)
|
||||
* **AI Integration:** `@google/genai` SDK
|
||||
* **Models Used:**
|
||||
* Text/Reasoning: `gemini-3-pro-preview`, `gemini-3-flash-preview`
|
||||
* Audio: `gemini-2.5-flash-preview-tts`
|
||||
* 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`
|
||||
* **Core:** React 19, TypeScript, Vite
|
||||
* **Styling:** Tailwind CSS, Lucide React, Framer Motion-style CSS animations
|
||||
* **AI SDK:** `@google/genai`
|
||||
* **Models:**
|
||||
* **Reasoning:** `gemini-3-pro-preview` / `gemini-3-flash-preview`
|
||||
* **Speech:** `gemini-2.5-flash-preview-tts`
|
||||
* **Vision:** `gemini-3-flash-preview` (OCR/Multimodal)
|
||||
* **Image Generation:** `imagen-4.0-generate-001`
|
||||
* **Image Editing:** `gemini-2.5-flash-image`
|
||||
* **Video Generation:** `veo-3.1-fast-generate-preview`
|
||||
|
||||
---
|
||||
|
||||
<a name="japanese"></a>
|
||||
## 🇯🇵 日本語
|
||||
|
||||
**さくら先生**は、Googleの最新Geminiモデルを搭載した没入型の日本語学習プラットフォームです。文法解説、ロールプレイ、読解・聴解練習など、あらゆる学習ニーズに対応します。
|
||||
**さくら先生**は、Googleの最新Geminiモデル(Gemini 3 / Veo)を搭載した、没入型の日本語学習プラットフォームです。文脈を理解するAIとの会話、発音のリアルタイム分析、そして生成AIによる教材作成機能で、あなたの日本語学習を加速させます。
|
||||
|
||||
### ✨ 主な機能
|
||||
|
||||
* **学習道場 (チャット):**
|
||||
* `gemini-3-pro-preview` を活用した高度な推論能力を持つAIチューターとの会話。
|
||||
* 音声入力・音声再生対応。思考モード(Thinking Mode)でAIの考え方を可視化。
|
||||
* チャット履歴の画像共有機能。
|
||||
* **読書の間:**
|
||||
* トピックと難易度(N5〜N1)を指定して、オリジナルの読み物を生成。
|
||||
* 単語リスト、文法解説、翻訳付き。テキストについてAIに質問可能。
|
||||
* **聴解ラボ:**
|
||||
* リスニング練習用の会話スクリプトとクイズを自動生成。
|
||||
* 音声再生、スクリプトの表示/非表示切り替え。
|
||||
* 文脈を記憶するAIチューターと自然な会話が可能。
|
||||
* **モデル切り替え機能:** 高速な `Gemini 3 Flash` と、深い推論を行う `Gemini 3 Pro` を用途に合わせて選択可能。
|
||||
* **Thinking Mode:** 複雑な文法解説を行う際、AIの思考プロセスを可視化。
|
||||
* **会話道場 (ロールプレイ):**
|
||||
* カフェ、駅、入国審査などのリアルなシナリオで会話練習。
|
||||
* 発音、流暢さ、文法ミスに対する即時フィードバック機能。
|
||||
* カフェ、駅、ホテルなど、リアルな場面での会話練習。
|
||||
* 発音や文法ミスに対する即時フィードバック。
|
||||
* **読書の間 & 聴解ラボ:**
|
||||
* トピックと難易度(N5〜N1)を指定して、あなただけの教材を自動生成。
|
||||
* 理解度クイズや、テキストに関する質疑応答機能付き。
|
||||
* **アトリエ:**
|
||||
* 画像生成 (`imagen-4.0`) や動画生成 (`veo-3.1`) で学習を視覚的にサポート。
|
||||
* `Imagen 3` による画像生成や `Veo` による動画生成で、視覚的に単語を記憶。
|
||||
* **ツールボックス:**
|
||||
* **スキャナー (OCR):** カメラや画像から日本語テキストを抽出し、解説を生成。
|
||||
* **翻訳機:** テキスト・画像の翻訳と音声再生。
|
||||
* **設定・データ:**
|
||||
* 学習履歴のローカル保存とバックアップ/復元機能。
|
||||
|
||||
### 🚀 始め方
|
||||
|
||||
1. **リポジトリのクローン:**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/sakura-sensei.git
|
||||
```
|
||||
|
||||
2. **依存関係のインストール:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **APIキーの設定:**
|
||||
* [Google AI Studio](https://aistudio.google.com/) でAPIキーを取得してください。
|
||||
* `.env` ファイルを作成するか、アプリ内の「設定」メニューからキーを入力します。
|
||||
|
||||
4. **起動:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
* **OCRスキャナー:** カメラで撮影した日本語テキストを瞬時に分析・解説。
|
||||
|
||||
---
|
||||
|
||||
<a name="chinese"></a>
|
||||
## 🇨🇳 中文
|
||||
|
||||
**樱花老师 (Sakura Sensei)** 是一个基于 Google Gemini 模型的全能型 AI 日语学习助手。它集成了对话练习、阅读生成、听力训练和实时纠错功能,为您提供沉浸式的日语学习体验。
|
||||
**樱花老师 (Sakura Sensei)** 是一款基于 Google 最新 Gemini 3 和 Veo 模型的下一代 AI 日语学习助手。它具备上下文记忆功能,提供深度推理、发音纠正以及多模态生成工具,为您打造身临其境的日语学习体验。
|
||||
|
||||
### ✨ 主要功能
|
||||
|
||||
* **学习道场 (Tutor Chat):**
|
||||
* 与 AI 导师自由对话,支持 `gemini-3-pro` 深度推理模式。
|
||||
* 支持语音输入 (STT) 和高质量语音朗读 (TTS)。
|
||||
* 支持将对话记录导出为图片、文本或文件。
|
||||
* **阅读室:**
|
||||
* 根据您感兴趣的主题和 JLPT 等级 (N5-N1) 生成阅读文章。
|
||||
* 自动提取词汇表、语法点和翻译。支持针对文章内容的提问。
|
||||
* **听力实验室:**
|
||||
* 生成包含理解测验的听力对话脚本。
|
||||
* 支持音频播放控制和脚本隐藏/显示。
|
||||
* **对话道场 (Roleplay):**
|
||||
* 在真实场景(如便利店、机场、酒店)中进行角色扮演。
|
||||
* AI 会对您的发音、流利度和语法进行打分并提供建议。
|
||||
* **学习道场 (智能对话):**
|
||||
* **双模型支持:** 在聊天界面直接切换 `Gemini 3 Flash` (极速) 和 `Gemini 3 Pro` (深度推理)。
|
||||
* **上下文记忆:** AI 能够记住之前的对话内容,提供连贯的辅导。
|
||||
* **思维链模式:** 可视化 AI 的思考过程,深度解析复杂语法。
|
||||
* **对话道场 (角色扮演):**
|
||||
* 模拟真实场景(如便利店、机场入境),提供实时语音评分与建议。
|
||||
* **阅读室 & 听力实验室:**
|
||||
* 根据您的 JLPT 等级 (N5-N1) 和兴趣,一键生成专属的阅读文章和听力测试。
|
||||
* **创意工作室:**
|
||||
* 使用 AI 生成图片或视频来辅助记忆单词和场景。
|
||||
* **实用工具:**
|
||||
* **扫描仪 (OCR):** 拍照识别日语文本,生成学习笔记。
|
||||
* **翻译机:** 支持文本和图片翻译,带发音功能。
|
||||
* **数据隐私:**
|
||||
* 所有聊天和学习记录均存储在本地浏览器中 (LocalStorage)。
|
||||
* 支持数据的备份与恢复 (JSON 格式)。
|
||||
|
||||
### 🚀 快速开始
|
||||
|
||||
1. **克隆项目:**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/sakura-sensei.git
|
||||
```
|
||||
|
||||
2. **安装依赖:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **配置 API Key:**
|
||||
* 前往 [Google AI Studio](https://aistudio.google.com/) 获取 Gemini API Key。
|
||||
* 在项目根目录创建 `.env` 文件,或直接在应用“设置”中输入 Key。
|
||||
|
||||
4. **运行应用:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
* 利用 `Imagen` 生成助记图片,或使用 `Veo` 生成短视频辅助学习。
|
||||
* **实用工具箱:**
|
||||
* **OCR 扫描:** 拍照识别日语文本,自动生成生词本和语法解析。
|
||||
|
||||
---
|
||||
|
||||
@@ -191,4 +135,4 @@
|
||||
|
||||
MIT License.
|
||||
|
||||
Powered by [Google Gemini API](https://ai.google.dev/).
|
||||
Powered by [Google Gemini API](https://ai.google.dev/).
|
||||
|
||||
@@ -28,6 +28,13 @@ const ChatBubble: React.FC<ChatBubbleProps> = ({ message, language, onUpdateMess
|
||||
const t = translations[language].chat;
|
||||
const tCommon = translations[language].common;
|
||||
|
||||
// Prevent rendering empty chat bubbles (e.g. initial placeholder before stream starts)
|
||||
// This ensures the "thinking" loader in ChatView is the only thing shown initially
|
||||
const isEmpty = !message.content?.trim() && !message.metadata?.imageUrl && !message.metadata?.audioUrl;
|
||||
if (isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stopAudio = () => {
|
||||
if (audioSourceRef.current) {
|
||||
audioSourceRef.current.stop();
|
||||
|
||||
BIN
releases/HTY1024-APP-SKR-0.11.0_20251229.zip
Normal file
BIN
releases/HTY1024-APP-SKR-0.11.0_20251229.zip
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
import { GoogleGenAI, Modality, Type } from "@google/genai";
|
||||
import { PronunciationFeedback, Language, ReadingLesson, ReadingDifficulty, OCRAnalysis, ListeningLesson } from "../types";
|
||||
import { PronunciationFeedback, Language, ReadingLesson, ReadingDifficulty, OCRAnalysis, ListeningLesson, ChatMessage, Role, MessageType } from "../types";
|
||||
import { base64ToUint8Array, uint8ArrayToBase64 } from "../utils/audioUtils";
|
||||
|
||||
export const USER_API_KEY_STORAGE = 'sakura_user_api_key';
|
||||
@@ -118,13 +118,24 @@ class GeminiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format history
|
||||
private _formatHistory(history: ChatMessage[]): any[] {
|
||||
return history
|
||||
.filter(msg => (msg.type === MessageType.TEXT || msg.type === MessageType.AUDIO) && msg.content)
|
||||
.map(msg => ({
|
||||
role: msg.role,
|
||||
parts: [{ text: msg.content }]
|
||||
}));
|
||||
}
|
||||
|
||||
private _getChatConfig(
|
||||
prompt: string,
|
||||
imageBase64?: string,
|
||||
useThinking: boolean = false,
|
||||
language: Language = 'en',
|
||||
modelOverride?: string,
|
||||
aiSpeakingLanguage: 'ja' | 'native' = 'native'
|
||||
aiSpeakingLanguage: 'ja' | 'native' = 'native',
|
||||
history: ChatMessage[] = []
|
||||
) {
|
||||
// Ensure model name is clean
|
||||
let modelName = useThinking
|
||||
@@ -135,20 +146,6 @@ class GeminiService {
|
||||
modelName = modelName.replace(/['"]/g, '');
|
||||
|
||||
const targetLangName = LANGUAGE_MAP[language];
|
||||
const parts: any[] = [];
|
||||
|
||||
if (imageBase64) {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/jpeg',
|
||||
data: imageBase64
|
||||
}
|
||||
});
|
||||
parts.push({ text: `Analyze this image in the context of learning Japanese. Explain in ${targetLangName}: ` + prompt });
|
||||
} else {
|
||||
parts.push({ text: prompt });
|
||||
}
|
||||
|
||||
let instruction = "";
|
||||
if (aiSpeakingLanguage === 'ja') {
|
||||
instruction = `You are Sakura, a Japanese language tutor.
|
||||
@@ -164,6 +161,32 @@ class GeminiService {
|
||||
- Provide your explanations, translations, and feedback in ${targetLangName}.`;
|
||||
}
|
||||
|
||||
// Build contents
|
||||
// 1. History
|
||||
const contents = this._formatHistory(history);
|
||||
|
||||
// 2. Current Turn
|
||||
const currentParts: any[] = [];
|
||||
|
||||
if (imageBase64) {
|
||||
const cleanBase64 = imageBase64.replace(/^data:image\/\w+;base64,/, "");
|
||||
currentParts.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/jpeg',
|
||||
data: cleanBase64
|
||||
}
|
||||
});
|
||||
currentParts.push({ text: `Analyze this image in the context of learning Japanese. Explain in ${targetLangName}: ` + prompt });
|
||||
} else {
|
||||
currentParts.push({ text: prompt });
|
||||
}
|
||||
|
||||
// Append current turn
|
||||
contents.push({
|
||||
role: 'user',
|
||||
parts: currentParts
|
||||
});
|
||||
|
||||
const config: any = {
|
||||
systemInstruction: instruction,
|
||||
};
|
||||
@@ -172,7 +195,7 @@ class GeminiService {
|
||||
config.thinkingConfig = { thinkingBudget: 32768 };
|
||||
}
|
||||
|
||||
return { modelName, parts, config };
|
||||
return { modelName, contents, config };
|
||||
}
|
||||
|
||||
// 1. Text Chat Response - Returns { text, model }
|
||||
@@ -182,15 +205,16 @@ class GeminiService {
|
||||
useThinking: boolean = false,
|
||||
language: Language = 'en',
|
||||
modelOverride?: string,
|
||||
aiSpeakingLanguage: 'ja' | 'native' = 'native'
|
||||
aiSpeakingLanguage: 'ja' | 'native' = 'native',
|
||||
history: ChatMessage[] = []
|
||||
): Promise<{ text: string, model: string }> {
|
||||
const ai = this.getAi();
|
||||
const { modelName, parts, config } = this._getChatConfig(prompt, imageBase64, useThinking, language, modelOverride, aiSpeakingLanguage);
|
||||
const { modelName, contents, config } = this._getChatConfig(prompt, imageBase64, useThinking, language, modelOverride, aiSpeakingLanguage, history);
|
||||
|
||||
return this.retryOperation(async () => {
|
||||
const response = await ai.models.generateContent({
|
||||
model: modelName,
|
||||
contents: { parts },
|
||||
contents: contents,
|
||||
config: config
|
||||
});
|
||||
return {
|
||||
@@ -207,16 +231,17 @@ class GeminiService {
|
||||
useThinking: boolean = false,
|
||||
language: Language = 'en',
|
||||
modelOverride?: string,
|
||||
aiSpeakingLanguage: 'ja' | 'native' = 'native'
|
||||
aiSpeakingLanguage: 'ja' | 'native' = 'native',
|
||||
history: ChatMessage[] = []
|
||||
): AsyncGenerator<{ text: string, model: string }> {
|
||||
const ai = this.getAi();
|
||||
const { modelName, parts, config } = this._getChatConfig(prompt, imageBase64, useThinking, language, modelOverride, aiSpeakingLanguage);
|
||||
const { modelName, contents, config } = this._getChatConfig(prompt, imageBase64, useThinking, language, modelOverride, aiSpeakingLanguage, history);
|
||||
|
||||
// Initial stream connection with retry logic
|
||||
const stream = await this.retryOperation(async () => {
|
||||
return await ai.models.generateContentStream({
|
||||
model: modelName,
|
||||
contents: { parts },
|
||||
contents: contents,
|
||||
config: config
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { ChatMessage, Role, MessageType, Language, ChatSession } from '../types';
|
||||
import { ChatMessage, Role, MessageType, Language, ChatSession, AVAILABLE_CHAT_MODELS } from '../types';
|
||||
import { geminiService } from '../services/geminiService';
|
||||
import ChatBubble from '../components/ChatBubble';
|
||||
import AudioRecorder from '../components/AudioRecorder';
|
||||
import { Send, Image as ImageIcon, BrainCircuit, Loader2, Plus, History, MessageSquare, Trash2, X, Sparkles, PanelRightClose, PanelRightOpen, Share2, Download, FileText, Image as ImageIconLucide, Languages } from 'lucide-react';
|
||||
import { Send, Image as ImageIcon, BrainCircuit, Loader2, Plus, History, MessageSquare, Trash2, X, Sparkles, PanelRightClose, PanelRightOpen, Share2, Download, FileText, Image as ImageIconLucide, Languages, ChevronDown } from 'lucide-react';
|
||||
import { translations } from '../utils/localization';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
@@ -18,7 +18,6 @@ interface ChatViewProps {
|
||||
onDeleteSession: (id: string) => void;
|
||||
onClearAllSessions: () => void;
|
||||
onUpdateSession: (id: string, messages: ChatMessage[]) => void;
|
||||
selectedModel?: string;
|
||||
addToast: (type: 'success' | 'error' | 'info', msg: string) => void;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ const ChatView: React.FC<ChatViewProps> = ({
|
||||
onDeleteSession,
|
||||
onClearAllSessions,
|
||||
onUpdateSession,
|
||||
selectedModel,
|
||||
addToast
|
||||
}) => {
|
||||
const t = translations[language].chat;
|
||||
@@ -45,6 +43,11 @@ const ChatView: React.FC<ChatViewProps> = ({
|
||||
const [useThinking, setUseThinking] = useState(false);
|
||||
const [attachedImage, setAttachedImage] = useState<string | null>(null);
|
||||
|
||||
// Local Model State
|
||||
const [selectedModel, setSelectedModel] = useState<string>(() => {
|
||||
return localStorage.getItem('sakura_chat_selected_model') || AVAILABLE_CHAT_MODELS[0].id;
|
||||
});
|
||||
|
||||
// Settings State
|
||||
const [aiSpeakingLanguage, setAiSpeakingLanguage] = useState<'ja' | 'native'>('ja');
|
||||
const [isShareMenuOpen, setIsShareMenuOpen] = useState(false);
|
||||
@@ -56,6 +59,10 @@ const ChatView: React.FC<ChatViewProps> = ({
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('sakura_chat_selected_model', selectedModel);
|
||||
}, [selectedModel]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
@@ -124,7 +131,8 @@ const ChatView: React.FC<ChatViewProps> = ({
|
||||
useThinking,
|
||||
language,
|
||||
selectedModel,
|
||||
aiSpeakingLanguage
|
||||
aiSpeakingLanguage,
|
||||
messages // Pass history (prior to current message)
|
||||
);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -199,7 +207,15 @@ const ChatView: React.FC<ChatViewProps> = ({
|
||||
// 4. Stream Response
|
||||
let fullText = "";
|
||||
let modelUsed = "";
|
||||
const stream = geminiService.generateTextStream(transcription, undefined, false, language, selectedModel, aiSpeakingLanguage);
|
||||
const stream = geminiService.generateTextStream(
|
||||
transcription,
|
||||
undefined,
|
||||
false,
|
||||
language,
|
||||
selectedModel,
|
||||
aiSpeakingLanguage,
|
||||
messages // Pass history
|
||||
);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
fullText += chunk.text;
|
||||
@@ -393,10 +409,24 @@ const ChatView: React.FC<ChatViewProps> = ({
|
||||
{/* Header / Toolbar */}
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-white/80 backdrop-blur border-b border-slate-200 z-20 sticky top-0">
|
||||
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold text-indigo-600 bg-indigo-50 px-2.5 py-1 rounded-lg border border-indigo-100 whitespace-nowrap">
|
||||
<Sparkles size={12} />
|
||||
{selectedModel ? selectedModel.replace('gemini-', '').replace('-preview', '') : 'AI'}
|
||||
</div>
|
||||
<div className="relative group z-10">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold text-indigo-600 bg-indigo-50 px-2.5 py-1 rounded-lg border border-indigo-100 whitespace-nowrap cursor-pointer hover:bg-indigo-100 transition-colors">
|
||||
<Sparkles size={12} />
|
||||
<span>
|
||||
{AVAILABLE_CHAT_MODELS.find(m => m.id === selectedModel)?.name.split('(')[0].trim() || 'Gemini'}
|
||||
</span>
|
||||
<ChevronDown size={10} className="opacity-50" />
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
||||
>
|
||||
{AVAILABLE_CHAT_MODELS.map(model => (
|
||||
<option key={model.id} value={model.id}>{model.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Language Toggle */}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user