更新至 v0.11.0_20251229 版本

This commit is contained in:
2025-12-29 22:23:48 +08:00
parent baba106935
commit cf75f2d60f
6 changed files with 161 additions and 163 deletions

14
App.tsx
View File

@@ -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
View File

@@ -1,5 +1,4 @@
# Sakura Sensei 🌸 - AI Japanese Tutor
![React](https://img.shields.io/badge/React-19.0-blue?logo=react)
@@ -8,7 +7,7 @@
![Vite](https://img.shields.io/badge/Vite-5.0-purple?logo=vite)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-3.4-cyan?logo=tailwindcss)
**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 (N5N1) 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/).

View File

@@ -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();

Binary file not shown.

View File

@@ -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
});
});

View File

@@ -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