Compare commits
No commits in common. "16720a035a92484267b803b6cbc207099300c3cf" and "2228f100006c03861ae364a162c39ed32ff04a41" have entirely different histories.
16720a035a
...
2228f10000
62 changed files with 365 additions and 4378 deletions
279
AGENTS.md
279
AGENTS.md
|
|
@ -1,279 +0,0 @@
|
|||
# Mistral Chat App - Development Context
|
||||
|
||||
## Project Overview
|
||||
|
||||
Android-приложение для чата с Mistral AI. Перспективный проект с развитием в сторону AI-агента с памятью, tools и автономной работой.
|
||||
|
||||
**Основные технологии:**
|
||||
- Kotlin + Android (minSdk 26, targetSdk 34)
|
||||
- Room + SQLCipher (encrypted database)
|
||||
- OkHttp для API
|
||||
- Material Design 3
|
||||
- Russian language UI
|
||||
|
||||
**Расположение проекта:**
|
||||
```
|
||||
/Users/alexabudaev/Documents/Zed/mistral-chat-app/
|
||||
```
|
||||
|
||||
**⚠️ ВАЖНО: Принципы разработки**
|
||||
- Приложение должно работать БЕЗ платных подписок и инвестиций
|
||||
- Всегда использовать собственные разработки или бесплатные решения
|
||||
- Не рассчитывать на имеющиеся платные API при планировании функций
|
||||
- Сначала находим бесплатное решение, потом реализуем
|
||||
- **Если что-то невозможно сделать без платных API или ты не можешь понять задачу - говори честно!**
|
||||
- При планировании любого следующего этапа следует тщательно анализировать возможность реализовать ту или иную функцию не только исходя из программной совместимости, но и с учётом наличия бесплатных версий необходимых сервисов, API и других продуктов (или возможности написать собственное решение)
|
||||
|
||||
---
|
||||
|
||||
## Completed Work
|
||||
|
||||
### ✅ Core Features
|
||||
- Чат с Mistral API (Chat Completion)
|
||||
- Управление профилями (до 10 профилей)
|
||||
- Управление сессиями (множественные чаты)
|
||||
- **Генерация названия сессии** - после 2-го сообщения AI генерирует краткое название (3-5 слов)
|
||||
- Шифрованное хранилище (SQLCipher + EncryptedSharedPreferences)
|
||||
- Валидация API ключа (32+ символов, A-Z, a-z, 0-9)
|
||||
- Левое drawer-меню с диалогами
|
||||
- Тёмная/светлая тема
|
||||
|
||||
### ✅ UI/UX
|
||||
- Material Design 3
|
||||
- Русский язык интерфейса
|
||||
- Отступы в поле ввода (12dp)
|
||||
- Прокрутка к новым сообщениям
|
||||
- **Долгий тап на сообщение** - меню Копировать/Редактировать/Удалить
|
||||
- **Скролл в поле system prompt** - multiline text field scrollable
|
||||
- **Адаптивные цвета AlertDialog** - Material Design 3 colors
|
||||
|
||||
### ✅ Bug Fixes
|
||||
- Исправлена ошибка "ответ в Toast вместо чата" - теперь только явные ошибки показываются через Toast
|
||||
- Сортировка drawer меню - профили, настройки, остальное
|
||||
- **WakeLock** - приложение остаётся активным при выключенном экране (ожидание ответа API)
|
||||
- **Timeout 120 сек** - увеличен с 60 до 120 секунд для больших ответов
|
||||
- **Foreground Service** - приложение продолжает работу в фоне при выключенном экране (ожидание ответа API)
|
||||
- **Уведомление о ответе ИИ** - при получении ответа в фоне показывается системное уведомление с текстом ответа и звуком/вибрацией, нажатие открывает сессию с ответом
|
||||
|
||||
### ✅ Security
|
||||
- API ключ: EncryptedSharedPreferences (AES-256-GCM)
|
||||
- Ключ БД: EncryptedSharedPreferences (AES-256-SIV + AES-256-GCM)
|
||||
- Профили, сессии, сообщения: SQLCipher
|
||||
- CalDAV данные: зашифрованы (url, username, password)
|
||||
|
||||
**⚠️ ВАЖНО: Все чувствительные данные должны храниться в EncryptedSharedPreferences:**
|
||||
- API ключи, пароли (CalDAV, email), ключи БД, токены
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
### 🌐 Web Search
|
||||
- ✅ Russian Wikipedia API (бесплатно, без API ключа)
|
||||
- ✅ English Wikipedia API
|
||||
- Ограничение: 4000 символов на ответ
|
||||
|
||||
### 🌤️ Weather
|
||||
- ✅ Open-Meteo API (полностью бесплатно)
|
||||
- Geocoding API + Weather API
|
||||
- Текущая погода + прогноз на 7 дней
|
||||
|
||||
### 🔗 OpenUrlTool
|
||||
- ✅ HTTP GET к любому URL
|
||||
- ✅ RSS/Atom парсинг (lenta.ru, kommersant.ru)
|
||||
- Ограничение: 4000 символов, таймаут 10 сек
|
||||
|
||||
### ⏰ Time Tools
|
||||
- ✅ get_local_time - возвращает timestamp в миллисекундах
|
||||
- ✅ get_date - текущая дата
|
||||
|
||||
### 📅 CalDAV Calendar
|
||||
- ✅ calendar_add_event - создание событий с VALARM (уведомления)
|
||||
- ✅ calendar_get_events - получение списка событий
|
||||
- ✅ calendar_delete_event - удаление событий
|
||||
- Баikal сервер интеграция
|
||||
|
||||
### 💾 Memory Tools
|
||||
- ✅ memory_store, memory_learn, memory_forget
|
||||
- ✅ memory_reinforce, memory_preference
|
||||
|
||||
---
|
||||
|
||||
## Tool Execution Parameters
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Max iterations | 15 |
|
||||
| Timeout на итерацию | 120 сек |
|
||||
| Retry при CANCEL | до 2 раз |
|
||||
| Result truncation | 2000 символов |
|
||||
| WakeLock | ✅ для длительных запросов |
|
||||
|
||||
---
|
||||
|
||||
## Active Plan
|
||||
|
||||
### Phase 3: CalDAV Calendar + Local Reminders (✅ В основном готово)
|
||||
|
||||
**Два режима:**
|
||||
1. **CalDAV** — синхронизация с Baikal сервером
|
||||
2. **Local** — автономная напоминалка в памяти AI
|
||||
|
||||
**CalDAV Status:**
|
||||
| Задача | Статус |
|
||||
|--------|--------|
|
||||
| Подключение к Baikal | ✅ |
|
||||
| calendar_add_event | ✅ Работает (с VALARM) |
|
||||
| calendar_get_events | ✅ Работает (лимит 100 событий) |
|
||||
| calendar_delete_event | ✅ Работает (только свои события) |
|
||||
| VALARM (уведомления) | ✅ Добавляются к событиям |
|
||||
| UID consistency | ✅ Исправлено |
|
||||
| Timestamp (time_string) | ✅ AI передаёт строку, сервер парсит |
|
||||
|
||||
**Нерешённые проблемы:**
|
||||
- При переустановке app старые события становятся "чужими" (новый UUID)
|
||||
|
||||
**Как помочь AI правильно работать с календарём:**
|
||||
1. Обязательно вызвать get_local_time для получения текущего UTC timestamp
|
||||
2. Использовать формулу: new_timestamp = current_timestamp + (часы * 3600000) + (минуты * 60000)
|
||||
3. НЕ добавлять случайные минуты!
|
||||
|
||||
### Phase 4: Heartbeat (⏳ В очереди)
|
||||
- WorkManager задача (каждые 30 минут)
|
||||
- Active hours (8:00-22:00)
|
||||
|
||||
### Phase 5: Email (⏳ В очереди)
|
||||
- IMAP/SMTP клиент (без OAuth)
|
||||
|
||||
### Phase 6: API Key Rotation (📋 Запланировано)
|
||||
|
||||
**Проблема:**
|
||||
- При достижении лимита токенов или блокировке ключа приложение перестаёт работать
|
||||
- Нужна система ротации для отказоустойчивости
|
||||
|
||||
**Механизм:**
|
||||
|
||||
1. **Хранение нескольких ключей:**
|
||||
- До 5 API ключей в EncryptedSharedPreferences
|
||||
- Каждый ключ имеет статус: active, disabled, blocked
|
||||
- Приоритет использования (порядок)
|
||||
|
||||
2. **Автоматическая ротация:**
|
||||
- При ошибке 429 (rate limit) → переключить на следующий ключ
|
||||
- При ошибке 401/403 (blocked) → пометить ключ как blocked
|
||||
- При успешном ответе → ключ working
|
||||
|
||||
3. **Логика переключения:**
|
||||
```
|
||||
При ошибке:
|
||||
- 429 (Too Many Requests) → nextKey()
|
||||
- 401/403 (Unauthorized) → markKeyBlocked(), nextKey()
|
||||
- 500+ → markKeyDisabled(), nextKey()
|
||||
|
||||
При успехе:
|
||||
- workingCount++ (счётчик успешных использований)
|
||||
```
|
||||
|
||||
4. **Ручное управление:**
|
||||
- UI для добавления/удаления ключей
|
||||
- Просмотр статуса каждого ключа
|
||||
- Ручное переключение
|
||||
|
||||
**UI реализация:**
|
||||
- Настройки профиля → "API ключи" → список ключей
|
||||
- Статус: ✅ рабочий, ⚠️ лимит, ❌ заблокирован
|
||||
- Возможность добавить/удалить/переключить
|
||||
|
||||
**Files to modify:**
|
||||
- `EncryptedPrefs.kt` - хранение нескольких ключей
|
||||
- `MistralClient.kt` - логика ротации
|
||||
- UI: dialog_settings.xml или новое диалоговое окно
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
- `app/src/main/java/com/mistral/chat/ui/MainActivity.kt` — главная активность
|
||||
- `app/src/main/java/com/mistral/chat/api/MistralClient.kt` — API клиент
|
||||
- `app/src/main/java/com/mistral/chat/api/ToolExecutor.kt` — менеджер tools
|
||||
- `app/src/main/java/com/mistral/chat/api/CalDavClient.kt` — CalDAV клиент
|
||||
- `app/src/main/java/com/mistral/chat/data/ChatDatabase.kt` — база данных
|
||||
|
||||
### Model Selection
|
||||
- **Default:** mistral-medium-latest
|
||||
- **OkHttp timeouts:** connect 60s, read 120s, write 60s
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ВАЖНЫЕ ПРАВИЛА РАЗРАБОТКИ
|
||||
|
||||
### Запрет на удаление реализованных функций
|
||||
**НИКОГДА не удаляй уже реализованные функции!**
|
||||
|
||||
### Запрет на хардкодинг
|
||||
**НИКОГДА не хардкодь значения, которые должны быть динамическими!**
|
||||
|
||||
### Сборка APK
|
||||
```bash
|
||||
JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew assembleDebug
|
||||
# Путь: app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Контекст сессии и оптимизация (📋 Запланировано)
|
||||
|
||||
### Текущая реализация
|
||||
При каждом запросе отправляется полный контекст. При росте сессии возможны 503 ошибки.
|
||||
|
||||
### План реализации (Kai 9000 style)
|
||||
|
||||
**Источник:** https://kai9000.com/docs/features/tools/
|
||||
|
||||
**Часть 1: Trimming (меж-итеративный)**
|
||||
- Обрезать историю ПОСЛЕ КАЖДОГО tool вызова
|
||||
- После каждого tool execution - проверить размер контекста
|
||||
- Если > MAX_CONTEXT - удалить старые сообщения (кроме system prompt)
|
||||
- MAX_CONTEXT = ~16000 токенов (50% от лимита mistral-medium)
|
||||
|
||||
**Часть 2: Compaction (AI summary)**
|
||||
- При 70% лимита (~22000 токенов) - запустить AI summary
|
||||
- Последние 4 user обмена - оставить verbatim
|
||||
- Остальное - одно summary message
|
||||
- Сохранить summary в БД для персистентности
|
||||
|
||||
### Реализация
|
||||
ToolExecutor.kt → модифицировать loop:
|
||||
```kotlin
|
||||
while (toolCalls.isNotEmpty()) {
|
||||
result = executeTool()
|
||||
messages.add(result)
|
||||
|
||||
// Trimming после каждого tool
|
||||
if (getTokenCount(messages) > MAX_CONTEXT) {
|
||||
trimOldMessages()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Files to modify
|
||||
- ToolExecutor.kt - добавить trimming в loop
|
||||
- MistralClient.kt - добавить getTokenCount, trimOldMessages
|
||||
|
||||
---
|
||||
|
||||
## Conversation Context (for AI Agent)
|
||||
|
||||
**При начале новой сессии:**
|
||||
Прочитай файл AGENTS.md для понимания текущего контекста.
|
||||
|
||||
**При запросе "продолжаем":**
|
||||
Мы работаем над Phase 3 - CalDAV календарь. Тестируем: создание событий, получение списка, исправление timezone.
|
||||
|
||||
**Важно:**
|
||||
- Пушить в GitHub только после подтверждения пользователя
|
||||
- Не делать push автоматически
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-04-12*
|
||||
*Version: 1.11*
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -41,20 +40,8 @@ dependencies {
|
|||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
implementation 'io.noties.markwon:ext-strikethrough:4.6.2'
|
||||
implementation 'io.noties.markwon:ext-tables:4.6.2'
|
||||
implementation 'io.noties.markwon:ext-tasklist:4.6.2'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.6.1'
|
||||
implementation 'androidx.room:room-ktx:2.6.1'
|
||||
ksp 'androidx.room:room-compiler:2.6.1'
|
||||
implementation 'net.zetetic:android-database-sqlcipher:4.5.4'
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.4.0'
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.mistral.chat.R
|
||||
|
||||
class ApiForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "api_service_channel"
|
||||
const val NOTIFICATION_ID = 1002 // Different from AI response notification
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, ApiForegroundService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, ApiForegroundService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Mistral Chat")
|
||||
.setContentText("Получение ответа от AI...")
|
||||
.setSmallIcon(android.R.drawable.ic_menu_send)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"API Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Уведомление о работе API"
|
||||
}
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +1,54 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.mistral.chat.data.Message
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
data class ChatResponse(
|
||||
val content: String,
|
||||
val usedModel: String,
|
||||
val toolCalls: List<ToolCall>
|
||||
)
|
||||
class MistralClient(private val apiKey: String) {
|
||||
|
||||
data class ToolCall(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val arguments: JsonObject
|
||||
)
|
||||
|
||||
class MistralClient(private val apiKey: String) {
|
||||
|
||||
private var client = createNewClient()
|
||||
private var toolExecutor: ToolExecutor? = null
|
||||
|
||||
private fun createNewClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val gson = Gson()
|
||||
private val jsonMediaType = "application/json".toMediaType()
|
||||
|
||||
private var currentCall: Call? = null
|
||||
private var currentContinuation: Continuation<Result<ChatResponse>>? = null
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://api.mistral.ai/v1"
|
||||
|
||||
private val SUPPORTED_MODELS = setOf(
|
||||
"mistral-small-latest",
|
||||
"mistral-medium-latest",
|
||||
"mistral-large-latest",
|
||||
"mistral-small",
|
||||
"mistral-medium",
|
||||
"mistral-large",
|
||||
"codestral-latest",
|
||||
"codestral",
|
||||
"pixtral-large-latest",
|
||||
"pixtral-12b-2409"
|
||||
)
|
||||
|
||||
val AVAILABLE_MODELS = listOf(
|
||||
"mistral-large-latest" to "Mistral Large",
|
||||
"mistral-small-latest" to "Mistral Small",
|
||||
"mistral-medium-latest" to "Mistral Medium",
|
||||
"codestral-latest" to "Codestral",
|
||||
"pixtral-large-latest" to "Pixtral Large"
|
||||
"mistral-large-latest" to "Mistral Large",
|
||||
"codestral-latest" to "Codestral"
|
||||
)
|
||||
|
||||
const val DEFAULT_MODEL = "mistral-medium-latest"
|
||||
}
|
||||
|
||||
fun setToolExecutor(executor: ToolExecutor) {
|
||||
this.toolExecutor = executor
|
||||
}
|
||||
|
||||
fun cancelRequest() {
|
||||
val call = currentCall
|
||||
val continuation = currentContinuation
|
||||
|
||||
call?.cancel()
|
||||
currentCall = null
|
||||
|
||||
continuation?.resume(Result.failure(Exception("Request cancelled")))
|
||||
currentContinuation = null
|
||||
|
||||
client = createNewClient()
|
||||
}
|
||||
|
||||
suspend fun getModels(): Result<List<Pair<String, String>>> = try {
|
||||
suspend fun getModels(): Result<List<Pair<String, String>>> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/models")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
return Result.failure(Exception("API error: ${response.code}"))
|
||||
return@withContext Result.failure(Exception("API error: ${response.code}"))
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
|
@ -112,59 +57,50 @@ class MistralClient(private val apiKey: String) {
|
|||
val models = responseJson
|
||||
.getAsJsonArray("data")
|
||||
?.mapNotNull { obj ->
|
||||
try {
|
||||
val jsonObj = obj.asJsonObject
|
||||
val id = jsonObj.get("id")?.asString ?: return@mapNotNull null
|
||||
if (id in SUPPORTED_MODELS) {
|
||||
val id = jsonObj.get("id")?.asString
|
||||
val created = jsonObj.get("created")?.asLong ?: 0L
|
||||
if (id != null && created > 0 && id.endsWith("-latest")) {
|
||||
val displayName = id
|
||||
.replace("-latest", "")
|
||||
.replace("-12b-2409", "")
|
||||
.replace("-", " ")
|
||||
.split(" ")
|
||||
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
||||
id to displayName
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.distinctBy { it.first }
|
||||
?: emptyList()
|
||||
} ?: emptyList()
|
||||
|
||||
Result.success(models)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelRequest() {
|
||||
currentCall?.cancel()
|
||||
currentCall = null
|
||||
}
|
||||
|
||||
suspend fun chat(
|
||||
model: String,
|
||||
messages: List<Message>,
|
||||
tools: List<JsonObject>? = null,
|
||||
onChunk: ((String) -> Unit)? = null
|
||||
): Result<ChatResponse> = withContext(Dispatchers.IO) {
|
||||
): Result<Pair<String, String>> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.addProperty("model", model)
|
||||
jsonObject.addProperty("temperature", 0.7)
|
||||
jsonObject.addProperty("stream", false)
|
||||
jsonObject.addProperty("stream", onChunk != null)
|
||||
|
||||
val messagesArray = JsonArray()
|
||||
messages.forEach { msg ->
|
||||
val msgObj = JsonObject()
|
||||
msgObj.addProperty("role", msg.role)
|
||||
msgObj.addProperty("role", if (msg.isUser) "user" else "assistant")
|
||||
msgObj.addProperty("content", msg.content)
|
||||
messagesArray.add(msgObj)
|
||||
}
|
||||
jsonObject.add("messages", messagesArray)
|
||||
|
||||
if (!tools.isNullOrEmpty()) {
|
||||
val toolsArray = JsonArray()
|
||||
tools.forEach { toolsArray.add(it) }
|
||||
jsonObject.add("tools", toolsArray)
|
||||
jsonObject.addProperty("tool_choice", "auto")
|
||||
}
|
||||
|
||||
val json = gson.toJson(jsonObject)
|
||||
val body = json.toRequestBody(jsonMediaType)
|
||||
|
||||
|
|
@ -176,41 +112,15 @@ class MistralClient(private val apiKey: String) {
|
|||
.build()
|
||||
|
||||
currentCall = client.newCall(request)
|
||||
val response = currentCall!!.execute()
|
||||
|
||||
val result = suspendCancellableCoroutine<Result<ChatResponse>> { continuation ->
|
||||
currentContinuation = continuation
|
||||
|
||||
currentCall?.enqueue(object : okhttp3.Callback {
|
||||
override fun onFailure(call: okhttp3.Call, e: java.io.IOException) {
|
||||
Log.e("MistralClient", "onFailure: ${e.message}", e)
|
||||
val cont = currentContinuation
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
if (cont != null) {
|
||||
if (call.isCanceled()) {
|
||||
cont.resume(Result.failure(Exception("Request cancelled")))
|
||||
} else {
|
||||
cont.resume(Result.failure(e))
|
||||
}
|
||||
}
|
||||
if (response.code == 0 || response.code == -1) {
|
||||
return@withContext Result.failure(Exception("Request cancelled"))
|
||||
}
|
||||
|
||||
override fun onResponse(call: okhttp3.Call, response: Response) {
|
||||
val cont = currentContinuation
|
||||
|
||||
if (cont == null) {
|
||||
response.close()
|
||||
currentCall = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!response.isSuccessful) {
|
||||
val errorBody = response.body?.string() ?: "Unknown error"
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(Exception("API error: ${response.code} - $errorBody")))
|
||||
return
|
||||
return@withContext Result.failure(Exception("API error: ${response.code} - $errorBody"))
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
|
@ -219,86 +129,26 @@ class MistralClient(private val apiKey: String) {
|
|||
onChunk(responseBody)
|
||||
}
|
||||
|
||||
val responseJson = try {
|
||||
gson.fromJson(responseBody, JsonObject::class.java)
|
||||
} catch (e: Exception) {
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(Exception("Invalid JSON response")))
|
||||
return
|
||||
}
|
||||
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
|
||||
|
||||
val choices = responseJson.getAsJsonArray("choices")
|
||||
if (choices == null || choices.size() == 0) {
|
||||
Log.e("MistralClient", "No choices in response: $responseBody")
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(Exception("No response from API")))
|
||||
return
|
||||
return@withContext Result.failure(Exception("No response from API"))
|
||||
}
|
||||
|
||||
val firstChoice = choices.get(0)?.asJsonObject
|
||||
if (firstChoice == null) {
|
||||
Log.e("MistralClient", "First choice is null")
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(Exception("Empty choice")))
|
||||
return
|
||||
}
|
||||
|
||||
val message = firstChoice.getAsJsonObject("message")
|
||||
val content = message?.get("content")?.asString ?: ""
|
||||
val content = choices
|
||||
.get(0)
|
||||
?.asJsonObject
|
||||
?.getAsJsonObject("message")
|
||||
?.get("content")
|
||||
?.asString ?: ""
|
||||
|
||||
val usedModel = responseJson.get("model")?.asString ?: model
|
||||
|
||||
val toolCalls = mutableListOf<ToolCall>()
|
||||
val toolCallsElement = message?.get("tool_calls")
|
||||
if (toolCallsElement != null && !toolCallsElement.isJsonNull) {
|
||||
val toolCallsArray = toolCallsElement.asJsonArray
|
||||
for ((index, tcElement) in toolCallsArray.withIndex()) {
|
||||
val tc = tcElement.asJsonObject
|
||||
if (tc != null) {
|
||||
val tcId = tc.get("id")?.asString ?: "call_${System.currentTimeMillis()}_$index"
|
||||
val tcName = tc.get("function")?.asJsonObject?.get("name")?.asString ?: ""
|
||||
val tcArgs = tc.get("function")?.asJsonObject?.get("arguments")?.asString
|
||||
if (tcName.isNotEmpty() && tcArgs != null) {
|
||||
try {
|
||||
val argsJson = gson.fromJson(tcArgs, JsonObject::class.java)
|
||||
toolCalls.add(ToolCall(tcId, tcName, argsJson))
|
||||
} catch (e: Exception) {
|
||||
// Skip invalid arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.success(ChatResponse(content, usedModel, toolCalls)))
|
||||
} catch (e: Exception) {
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
cont.resume(Result.failure(e))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
currentCall?.cancel()
|
||||
currentCall = null
|
||||
currentContinuation = null
|
||||
client = createNewClient()
|
||||
}
|
||||
}
|
||||
result
|
||||
Result.success(content to usedModel)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun executeTool(name: String, arguments: JsonObject): String {
|
||||
return toolExecutor?.executeTool(name, arguments)
|
||||
?: """{"status": "error", "message": "Tool executor not initialized"}"""
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
class NotificationTool(private val context: Context) : Tool(
|
||||
name = "send_notification",
|
||||
description = "Отправить уведомление пользователю. Используй когда нужно сообщить важную информацию или напомнить о чём-то.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("title", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Заголовок уведомления"))
|
||||
})
|
||||
add("message", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Текст уведомления"))
|
||||
})
|
||||
})
|
||||
add("required", com.google.gson.JsonArray().apply {
|
||||
add("title")
|
||||
add("message")
|
||||
})
|
||||
}
|
||||
) {
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "mistral_chat_notifications"
|
||||
private const val CHANNEL_NAME = "Chat Notifications"
|
||||
}
|
||||
|
||||
override suspend fun execute(arguments: JsonObject): String {
|
||||
val title = arguments.get("title")?.asString ?: "Уведомление"
|
||||
val message = arguments.get("message")?.asString ?: ""
|
||||
|
||||
if (message.isEmpty()) {
|
||||
return """{"status": "error", "message": "Message cannot be empty"}"""
|
||||
}
|
||||
|
||||
// Проверка разрешения для Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = android.Manifest.permission.POST_NOTIFICATIONS
|
||||
if (context.checkSelfPermission(permission) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
return """{"status": "error", "message": "permission_denied: Уведомления отключены в настройках. Попроси пользователя включить их в настройках приложения."}"""
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Уведомления от Mistral Chat"
|
||||
enableVibration(true)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val notification = android.app.Notification.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(android.app.Notification.BigTextStyle().bigText(message))
|
||||
.setPriority(android.app.Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
|
||||
|
||||
"""{"status": "success", "message": "Уведомление отправлено: $title"}"""
|
||||
} catch (e: Exception) {
|
||||
"""{"status": "error", "message": "${e.message}"}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,488 +0,0 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class GetTimeTool(
|
||||
private val getDefaultTimezone: () -> String
|
||||
) : Tool(
|
||||
name = "get_local_time",
|
||||
description = "Получить текущую дату и время.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("timezone", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Часовой пояс (опционально)"))
|
||||
})
|
||||
add("format", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Формат даты/времени (опционально). Пример: 'dd MMMM yyyy, HH:mm'"))
|
||||
})
|
||||
})
|
||||
}
|
||||
) {
|
||||
override suspend fun execute(arguments: JsonObject): String {
|
||||
val timezone = arguments.get("timezone")?.asString ?: getDefaultTimezone()
|
||||
val format = arguments.get("format")?.asString ?: "dd MMMM yyyy, HH:mm"
|
||||
|
||||
return try {
|
||||
val sdf = SimpleDateFormat(format, Locale("ru", "RU"))
|
||||
sdf.timeZone = java.util.TimeZone.getTimeZone(timezone)
|
||||
val now = Date()
|
||||
val formatted = sdf.format(now)
|
||||
"""{"status": "success", "time": "$formatted", "timezone": "$timezone"}"""
|
||||
} catch (e: Exception) {
|
||||
"""{"status": "error", "message": "${e.message}"}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetDateTool(
|
||||
private val getDefaultTimezone: () -> String
|
||||
) : Tool(
|
||||
name = "get_date",
|
||||
description = "Получить текущую дату. Используй когда пользователь спрашивает какое сегодня число.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("format", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Формат даты (опционально). Пример: 'dd MMMM yyyy'"))
|
||||
})
|
||||
})
|
||||
}
|
||||
) {
|
||||
override suspend fun execute(arguments: JsonObject): String {
|
||||
val format = arguments.get("format")?.asString ?: "dd MMMM yyyy"
|
||||
val timezone = getDefaultTimezone()
|
||||
|
||||
return try {
|
||||
val sdf = SimpleDateFormat(format, Locale("ru", "RU"))
|
||||
sdf.timeZone = java.util.TimeZone.getTimeZone(timezone)
|
||||
val now = Date()
|
||||
"""{"status": "success", "date": "${sdf.format(now)}"}"""
|
||||
} catch (e: Exception) {
|
||||
"""{"status": "error", "message": "${e.message}"}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetWeatherTool(
|
||||
private val getDefaultCity: () -> String
|
||||
) : Tool(
|
||||
name = "get_weather",
|
||||
description = "Получить текущую погоду и прогноз на 7 дней в указанном городе.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("city", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Город для прогноза погоды"))
|
||||
})
|
||||
})
|
||||
}
|
||||
) {
|
||||
private val httpClient = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override suspend fun execute(arguments: JsonObject): String = withContext(Dispatchers.IO) {
|
||||
val city = arguments.get("city")?.asString ?: getDefaultCity()
|
||||
getWeather(city)
|
||||
}
|
||||
|
||||
private fun getCityCoords(cityName: String): Pair<Double, Double>? {
|
||||
return try {
|
||||
val encodedCity = java.net.URLEncoder.encode(cityName, "UTF-8")
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url("https://geocoding-api.open-meteo.com/v1/search?name=$encodedCity&count=1&language=ru&format=json")
|
||||
.get()
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
val body = response.body?.string() ?: ""
|
||||
val json = com.google.gson.JsonParser.parseString(body).asJsonObject
|
||||
val results = json.get("results")?.asJsonArray
|
||||
if (results != null && results.size() > 0) {
|
||||
val lat = results[0].asJsonObject.get("latitude").asDouble
|
||||
val lon = results[0].asJsonObject.get("longitude").asDouble
|
||||
Pair(lat, lon)
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWeather(cityName: String): String {
|
||||
val coords = getCityCoords(cityName) ?: return """{"status": "error", "message": "Город не найден"}"""
|
||||
return try {
|
||||
val (lat, lon) = coords
|
||||
|
||||
// Запрашиваем текущую погоду + прогноз на 7 дней
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url("https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon¤t=temperature_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7")
|
||||
.get()
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
val body = response.body?.string() ?: ""
|
||||
val json = com.google.gson.JsonParser.parseString(body).asJsonObject
|
||||
|
||||
val current = json.get("current")?.asJsonObject
|
||||
val daily = json.get("daily")?.asJsonObject
|
||||
|
||||
if (current != null && daily != null) {
|
||||
// Текущая погода
|
||||
val temp = current.get("temperature_2m")?.asDouble ?: 0.0
|
||||
val wind = current.get("wind_speed_10m")?.asDouble ?: 0.0
|
||||
val code = current.get("weather_code")?.asInt ?: 0
|
||||
val weather = getWeatherDescription(code)
|
||||
|
||||
// Прогноз на 7 дней
|
||||
val dailyTime = daily.get("time")?.asJsonArray
|
||||
val dailyMaxTemp = daily.get("temperature_2m_max")?.asJsonArray
|
||||
val dailyMinTemp = daily.get("temperature_2m_min")?.asJsonArray
|
||||
val dailyCode = daily.get("weather_code")?.asJsonArray
|
||||
val dailyPrecip = daily.get("precipitation_sum")?.asJsonArray
|
||||
val dailyPrecipProb = daily.get("precipitation_probability_max")?.asJsonArray
|
||||
|
||||
val forecastLines = mutableListOf<String>()
|
||||
|
||||
if (dailyTime != null && dailyMaxTemp != null) {
|
||||
for (i in 0 until minOf(dailyTime.size(), 7)) {
|
||||
val date = dailyTime.get(i).asString?.takeLast(5) ?: ""
|
||||
val maxTemp = dailyMaxTemp.get(i).asDouble ?: 0.0
|
||||
val minTemp = dailyMinTemp?.get(i)?.asDouble ?: maxTemp
|
||||
val dayCode = dailyCode?.get(i)?.asInt ?: 0
|
||||
val precip = dailyPrecip?.get(i)?.asDouble ?: 0.0
|
||||
val precipProb = dailyPrecipProb?.get(i)?.asInt ?: 0
|
||||
|
||||
val dayWeather = getWeatherDescription(dayCode)
|
||||
val dayName = getDayName(i)
|
||||
|
||||
forecastLines.add("$dayName ($date): макс $maxTemp°C, мин $minTemp°C, $dayWeather, осадки ${precip}мм ($precipProb%)")
|
||||
}
|
||||
}
|
||||
|
||||
val forecastText = if (forecastLines.isNotEmpty()) {
|
||||
"\n\nПрогноз на 7 дней:\n" + forecastLines.joinToString("\n")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
"""Текущая погода в $cityName: $temp°C, $weather, ветер ${wind}km/h$forecastText"""
|
||||
} else {
|
||||
"""{"status": "error", "message": "Не удалось получить погоду"}"""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("Weather", "Error: ${e.message}", e)
|
||||
"""{"status": "error", "message": "Ошибка получения погоды: ${e.message}"}"""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDayName(dayIndex: Int): String {
|
||||
return when (dayIndex) {
|
||||
0 -> "Сегодня"
|
||||
1 -> "Завтра"
|
||||
2 -> "Послезавтра"
|
||||
else -> "День ${dayIndex + 1}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWeatherDescription(code: Int): String {
|
||||
return when (code) {
|
||||
0 -> "Ясно"
|
||||
1, 2, 3 -> "Облачно"
|
||||
45, 48 -> "Туман"
|
||||
51, 53, 55 -> "Морось"
|
||||
56, 57 -> "Ледяная морось"
|
||||
61, 63, 65 -> "Дождь"
|
||||
66, 67 -> "Ледяной дождь"
|
||||
71, 73, 75 -> "Снег"
|
||||
77 -> "Снежные зёрна"
|
||||
80, 81, 82 -> "Ливень"
|
||||
85, 86 -> "Снегопад"
|
||||
95 -> "Гроза"
|
||||
96, 99 -> "Гроза с градом"
|
||||
else -> "Неизвестно"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WebSearchTool(
|
||||
private val getDefaultCity: () -> String
|
||||
) : Tool(
|
||||
name = "web_search",
|
||||
description = "Поиск информации в Wikipedia (русской и английской). Проверяй обе версии для получения актуальной информации.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("query", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Поисковый запрос"))
|
||||
})
|
||||
add("location", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("Место (опционально). По умолчанию - ${getDefaultCity()}"))
|
||||
})
|
||||
add("num_results", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("number"))
|
||||
add("description", com.google.gson.JsonPrimitive("Количество результатов (по умолчанию 10)"))
|
||||
})
|
||||
})
|
||||
add("required", com.google.gson.JsonArray().apply {
|
||||
add("query")
|
||||
})
|
||||
}
|
||||
) {
|
||||
private val httpClient = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override suspend fun execute(arguments: JsonObject): String = withContext(Dispatchers.IO) {
|
||||
val query = arguments.get("query")?.asString
|
||||
val location = arguments.get("location")?.asString ?: getDefaultCity()
|
||||
val numResults = arguments.get("num_results")?.asInt ?: 10
|
||||
|
||||
if (query.isNullOrEmpty()) {
|
||||
return@withContext """{"status": "error", "message": "Query is required"}"""
|
||||
}
|
||||
|
||||
try {
|
||||
// Ищем и в русской, и в английской Wikipedia параллельно
|
||||
val ruResults = coroutineScope {
|
||||
async { searchWikipedia(query, numResults, "ru") }.await()
|
||||
}
|
||||
val enResults = coroutineScope {
|
||||
async { searchWikipedia(query, numResults, "en") }.await()
|
||||
}
|
||||
|
||||
val allResults = mutableListOf<String>()
|
||||
|
||||
if (ruResults.isNotEmpty()) {
|
||||
allResults.add("=== РУССКАЯ WIKIPEDIA ===")
|
||||
allResults.addAll(ruResults)
|
||||
}
|
||||
|
||||
if (enResults.isNotEmpty()) {
|
||||
allResults.add("=== ENGLISH WIKIPEDIA ===")
|
||||
allResults.addAll(enResults)
|
||||
}
|
||||
|
||||
if (allResults.isEmpty()) {
|
||||
"""{"status": "success", "message": "Ничего не найдено по запросу '$query'"}"""
|
||||
} else {
|
||||
val responseText = allResults.joinToString("\n\n").take(4000)
|
||||
"""Найденная информация:\n\n$responseText"""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"""{"status": "error", "message": "Search failed: ${e.message}"}"""
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchWikipedia(query: String, limit: Int, lang: String): List<String> {
|
||||
val results = mutableListOf<String>()
|
||||
|
||||
try {
|
||||
val encodedQuery = java.net.URLEncoder.encode(query, "UTF-8")
|
||||
val wikiLang = if (lang == "en") "en" else "ru"
|
||||
|
||||
val searchRequest = okhttp3.Request.Builder()
|
||||
.url("https://$wikiLang.wikipedia.org/w/api.php?action=query&list=search&srsearch=$encodedQuery&srlimit=$limit&format=json&origin=*")
|
||||
.header("User-Agent", "MistralChat/1.0")
|
||||
.header("Accept", "application/json")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val searchResponse = httpClient.newCall(searchRequest).execute()
|
||||
val searchBody = searchResponse.body?.string() ?: ""
|
||||
|
||||
val json = com.google.gson.JsonParser.parseString(searchBody).asJsonObject
|
||||
val queryObj = json.get("query")?.asJsonObject
|
||||
val searchArray = queryObj?.get("search")?.asJsonArray
|
||||
|
||||
if (searchArray != null && searchArray.size() > 0) {
|
||||
for (i in 0 until minOf(searchArray.size(), limit)) {
|
||||
val item = searchArray[i].asJsonObject
|
||||
val title = item.get("title")?.asString ?: ""
|
||||
val snippet = item.get("snippet")?.asString ?: ""
|
||||
|
||||
if (title.isNotEmpty()) {
|
||||
val cleanSnippet = snippet
|
||||
.replace(Regex("<[^>]*>"), "")
|
||||
.replace(""", "\"")
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
|
||||
val text = if (cleanSnippet.isNotEmpty()) {
|
||||
"Статья: $title\nСодержание: $cleanSnippet"
|
||||
} else {
|
||||
"Статья: $title"
|
||||
}
|
||||
results.add(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Игнорируем ошибки поиска
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
class OpenUrlTool : Tool(
|
||||
name = "open_url",
|
||||
description = "Получить текст с веб-страницы или RSS-ленты по URL. Автоматически определяет RSS и парсит заголовки новостей.",
|
||||
inputSchema = JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("object"))
|
||||
add("properties", JsonObject().apply {
|
||||
add("url", JsonObject().apply {
|
||||
add("type", com.google.gson.JsonPrimitive("string"))
|
||||
add("description", com.google.gson.JsonPrimitive("URL страницы или RSS-ленты (например: https://lenta.ru/rss/, https://www.kommersant.ru/rss/news.xml)"))
|
||||
})
|
||||
})
|
||||
add("required", com.google.gson.JsonArray().apply {
|
||||
add("url")
|
||||
})
|
||||
}
|
||||
) {
|
||||
private val httpClient = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override suspend fun execute(arguments: JsonObject): String = withContext(Dispatchers.IO) {
|
||||
val url = arguments.get("url")?.asString
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
return@withContext """{"status": "error", "message": "URL is required"}"""
|
||||
}
|
||||
|
||||
val normalizedUrl = url.lowercase()
|
||||
if (normalizedUrl.startsWith("javascript:") ||
|
||||
normalizedUrl.startsWith("file:") ||
|
||||
normalizedUrl.startsWith("data:") ||
|
||||
normalizedUrl.startsWith("mailto:") ||
|
||||
normalizedUrl.startsWith("tel:")) {
|
||||
return@withContext """{"status": "error", "message": "Недопустимый тип URL"}"""
|
||||
}
|
||||
|
||||
try {
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Android)")
|
||||
.header("Accept", "application/rss+xml,application/atom+xml,application/xml,text/xml,text/html,application/xhtml+xml")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
return@withContext """{"status": "error", "message": "Ошибка HTTP: ${response.code}"}"""
|
||||
}
|
||||
|
||||
val contentType = response.header("Content-Type") ?: ""
|
||||
val body = response.body?.string() ?: ""
|
||||
|
||||
val isRss = contentType.contains("xml") || body.trim().startsWith("<?xml") || body.trim().startsWith("<rss") || body.trim().startsWith("<feed")
|
||||
|
||||
val textOnly = if (isRss) {
|
||||
parseRssFeed(body)
|
||||
} else {
|
||||
body
|
||||
.replace(Regex("<script[^>]*>.*?</script>", RegexOption.DOT_MATCHES_ALL), "")
|
||||
.replace(Regex("<style[^>]*>.*?</style>", RegexOption.DOT_MATCHES_ALL), "")
|
||||
.replace(Regex("<[^>]+>"), " ")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.replace(" ", " ")
|
||||
.replace(""", "\"")
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("—", "—")
|
||||
.replace("–", "–")
|
||||
.trim()
|
||||
}
|
||||
|
||||
val result = textOnly.take(2000)
|
||||
"""{"status": "success", "content": "$result"}"""
|
||||
} catch (e: Exception) {
|
||||
"""{"status": "error", "message": "Ошибка загрузки: ${e.message}"""
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRssFeed(xml: String): String {
|
||||
val items = mutableListOf<String>()
|
||||
|
||||
try {
|
||||
val cleanXml = xml
|
||||
.replace(Regex("<!\\[CDATA\\[", RegexOption.DOT_MATCHES_ALL), "")
|
||||
.replace(Regex("]]>", RegexOption.DOT_MATCHES_ALL), "")
|
||||
|
||||
val titleMatch = Regex("<title><!\\[CDATA\\[(.*?)\\]\\]></title>|<title>(.*?)</title>", RegexOption.DOT_MATCHES_ALL).find(cleanXml)
|
||||
val feedTitle = titleMatch?.let { it.groupValues[1].ifEmpty { it.groupValues[2] } } ?: ""
|
||||
|
||||
val itemRegex = Regex(
|
||||
"<item>|<entry>",
|
||||
RegexOption.DOT_MATCHES_ALL
|
||||
)
|
||||
|
||||
val itemMatches = itemRegex.findAll(cleanXml)
|
||||
|
||||
for ((index, match) in itemMatches.withIndex()) {
|
||||
if (index >= 15) break
|
||||
|
||||
val start = match.range.first
|
||||
val endRange = if (index + 1 < itemMatches.count()) {
|
||||
itemRegex.findAll(cleanXml).toList()[index + 1].range.first
|
||||
} else {
|
||||
cleanXml.length
|
||||
}
|
||||
|
||||
val itemXml = cleanXml.substring(start, endRange)
|
||||
|
||||
val itemTitle = Regex("<title><!\\[CDATA\\[(.*?)\\]\\]></title>|<title>(.*?)</title>", RegexOption.DOT_MATCHES_ALL)
|
||||
.find(itemXml)?.let { it.groupValues[1].ifEmpty { it.groupValues[2] } } ?: ""
|
||||
|
||||
val itemLink = Regex("<link>(.*?)</link>").find(itemXml)?.groupValues?.getOrNull(1) ?: ""
|
||||
|
||||
val itemDesc = Regex("<description><!\\[CDATA\\[(.*?)\\]\\]></description>|<description>(.*?)</description>", RegexOption.DOT_MATCHES_ALL)
|
||||
.find(itemXml)?.let { it.groupValues[1].ifEmpty { it.groupValues[2] } } ?: ""
|
||||
|
||||
val itemDate = Regex("<pubDate>|<published>").find(itemXml)?.let { dateMatch ->
|
||||
val dateStart = dateMatch.range.last + 1
|
||||
val dateEnd = minOf(dateStart + 50, cleanXml.length)
|
||||
val dateSection = cleanXml.substring(dateStart, dateEnd)
|
||||
Regex("(<[^>]+>)").replace(dateSection, "").trim()
|
||||
} ?: ""
|
||||
|
||||
if (itemTitle.isNotEmpty()) {
|
||||
val itemText = buildString {
|
||||
append("• $itemTitle")
|
||||
if (itemDate.isNotEmpty()) append(" [$itemDate]")
|
||||
if (itemLink.isNotEmpty()) append(" | ${itemLink}")
|
||||
if (itemDesc.isNotEmpty() && itemDesc.length < 150) append(" - $itemDesc")
|
||||
}
|
||||
items.add(itemText)
|
||||
}
|
||||
}
|
||||
|
||||
if (feedTitle.isNotEmpty()) {
|
||||
return "=== $feedTitle ===\n\n" + items.joinToString("\n")
|
||||
}
|
||||
return items.joinToString("\n")
|
||||
|
||||
} catch (e: Exception) {
|
||||
return "Ошибка парсинга RSS: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import net.sqlcipher.database.SupportFactory
|
||||
|
||||
@Database(
|
||||
entities = [Profile::class, Session::class, MessageEntity::class, Setting::class],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class ChatDatabase : RoomDatabase() {
|
||||
abstract fun profileDao(): ProfileDao
|
||||
abstract fun sessionDao(): SessionDao
|
||||
abstract fun messageDao(): MessageDao
|
||||
abstract fun settingDao(): SettingDao
|
||||
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "mistral_chat.db"
|
||||
|
||||
@Volatile
|
||||
private var INSTANCE: ChatDatabase? = null
|
||||
|
||||
fun getInstance(context: Context): ChatDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDatabase(context: Context): ChatDatabase {
|
||||
val passphrase = getOrCreatePassphrase(context)
|
||||
val factory = SupportFactory(passphrase)
|
||||
|
||||
return Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
ChatDatabase::class.java,
|
||||
DATABASE_NAME
|
||||
)
|
||||
.openHelperFactory(factory)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getOrCreatePassphrase(context: Context): ByteArray {
|
||||
val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
val securePrefs = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"db_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
val existingKey = securePrefs.getString("db_key", null)
|
||||
|
||||
return if (existingKey != null) {
|
||||
existingKey.toByteArray(Charsets.UTF_8)
|
||||
} else {
|
||||
val newKey = generateSecureKey()
|
||||
securePrefs.edit().putString("db_key", newKey).apply()
|
||||
newKey.toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateSecureKey(): String {
|
||||
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"
|
||||
return (1..32).map { chars.random() }.joinToString("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +1,27 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "messages",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Session::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["sessionId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index("sessionId")]
|
||||
)
|
||||
data class MessageEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val sessionId: Long,
|
||||
val content: String,
|
||||
val isUser: Boolean,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
data class Message(
|
||||
val id: Long = 0,
|
||||
val sessionId: Long = 0,
|
||||
val id: String = System.currentTimeMillis().toString(),
|
||||
val content: String,
|
||||
val isUser: Boolean,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val senderName: String? = null
|
||||
)
|
||||
|
||||
fun MessageEntity.toMessage(): Message = Message(
|
||||
id = id,
|
||||
sessionId = sessionId,
|
||||
content = content,
|
||||
isUser = isUser,
|
||||
timestamp = timestamp
|
||||
data class ChatRequest(
|
||||
val model: String,
|
||||
val messages: List<Message>,
|
||||
val temperature: Double = 0.7,
|
||||
val stream: Boolean = false
|
||||
)
|
||||
|
||||
fun Message.toEntity(): MessageEntity = MessageEntity(
|
||||
id = id,
|
||||
sessionId = sessionId,
|
||||
content = content,
|
||||
isUser = isUser,
|
||||
timestamp = timestamp
|
||||
data class ChatResponse(
|
||||
val id: String,
|
||||
val choices: List<Choice>,
|
||||
val model: String
|
||||
)
|
||||
|
||||
data class Choice(
|
||||
val index: Int,
|
||||
val message: Message
|
||||
)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface MessageDao {
|
||||
@Query("SELECT * FROM messages WHERE sessionId = :sessionId ORDER BY timestamp ASC")
|
||||
fun getMessagesBySession(sessionId: Long): Flow<List<MessageEntity>>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE sessionId = :sessionId ORDER BY timestamp ASC")
|
||||
suspend fun getMessagesBySessionSync(sessionId: Long): List<MessageEntity>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM messages WHERE sessionId = :sessionId")
|
||||
suspend fun getMessageCount(sessionId: Long): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(message: MessageEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(messages: List<MessageEntity>)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(message: MessageEntity)
|
||||
|
||||
@Query("DELETE FROM messages WHERE sessionId = :sessionId")
|
||||
suspend fun deleteBySession(sessionId: Long)
|
||||
|
||||
@Query("DELETE FROM messages")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "profiles")
|
||||
data class Profile(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val name: String,
|
||||
val bio: String = "",
|
||||
val preferences: String = "",
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ProfileDao {
|
||||
@Query("SELECT * FROM profiles ORDER BY updatedAt DESC")
|
||||
fun getAllProfiles(): Flow<List<Profile>>
|
||||
|
||||
@Query("SELECT * FROM profiles WHERE id = :id")
|
||||
suspend fun getProfileById(id: Long): Profile?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(profile: Profile): Long
|
||||
|
||||
@Update
|
||||
suspend fun update(profile: Profile)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(profile: Profile)
|
||||
|
||||
@Query("DELETE FROM profiles WHERE id = :id")
|
||||
suspend fun deleteById(id: Long)
|
||||
|
||||
@Query("DELETE FROM profiles")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "sessions",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Profile::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["profileId"],
|
||||
onDelete = ForeignKey.SET_NULL
|
||||
)
|
||||
],
|
||||
indices = [Index("profileId")]
|
||||
)
|
||||
data class Session(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val profileId: Long? = null,
|
||||
val title: String = "Новая сессия",
|
||||
val isManuallyRenamed: Boolean = false,
|
||||
val isTitleGenerated: Boolean = false,
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SessionDao {
|
||||
@Query("SELECT * FROM sessions ORDER BY updatedAt DESC")
|
||||
fun getAllSessions(): Flow<List<Session>>
|
||||
|
||||
@Query("SELECT * FROM sessions WHERE profileId = :profileId ORDER BY updatedAt DESC")
|
||||
fun getSessionsByProfile(profileId: Long): Flow<List<Session>>
|
||||
|
||||
@Query("SELECT * FROM sessions WHERE profileId IS NULL ORDER BY updatedAt DESC")
|
||||
fun getSessionsWithoutProfile(): Flow<List<Session>>
|
||||
|
||||
@Query("SELECT * FROM sessions WHERE id = :id")
|
||||
suspend fun getSessionById(id: Long): Session?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(session: Session): Long
|
||||
|
||||
@Update
|
||||
suspend fun update(session: Session)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(session: Session)
|
||||
|
||||
@Query("DELETE FROM sessions WHERE id = :id")
|
||||
suspend fun deleteById(id: Long)
|
||||
|
||||
@Query("DELETE FROM sessions")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Query("UPDATE sessions SET updatedAt = :timestamp WHERE id = :sessionId")
|
||||
suspend fun updateTimestamp(sessionId: Long, timestamp: Long = System.currentTimeMillis())
|
||||
|
||||
@Query("UPDATE sessions SET title = :title, isManuallyRenamed = 1 WHERE id = :sessionId")
|
||||
suspend fun updateTitle(sessionId: Long, title: String)
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "settings")
|
||||
data class Setting(
|
||||
@PrimaryKey
|
||||
val key: String,
|
||||
val value: String
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SettingDao {
|
||||
@Query("SELECT * FROM settings WHERE `key` = :key")
|
||||
suspend fun getSetting(key: String): Setting?
|
||||
|
||||
@Query("SELECT value FROM settings WHERE `key` = :key")
|
||||
suspend fun getValue(key: String): String?
|
||||
|
||||
@Query("SELECT value FROM settings WHERE `key` = :key")
|
||||
fun getValueFlow(key: String): Flow<String?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(setting: Setting)
|
||||
|
||||
@Query("DELETE FROM settings WHERE `key` = :key")
|
||||
suspend fun delete(key: String)
|
||||
}
|
||||
18
app/src/main/java/com/mistral/chat/data/UserProfile.kt
Normal file
18
app/src/main/java/com/mistral/chat/data/UserProfile.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
data class UserProfile(
|
||||
val name: String = "",
|
||||
val bio: String = "",
|
||||
val preferences: String = ""
|
||||
) {
|
||||
fun isEmpty(): Boolean = name.isBlank() && bio.isBlank() && preferences.isBlank()
|
||||
|
||||
fun toContextString(): String {
|
||||
return buildString {
|
||||
append("[User Profile]\n")
|
||||
if (name.isNotBlank()) append("Name: $name\n")
|
||||
if (bio.isNotBlank()) append("Bio: $bio\n")
|
||||
if (preferences.isNotBlank()) append("Preferences: $preferences\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package com.mistral.chat.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mistral.chat.R
|
||||
|
||||
class DrawerSubmenuAdapter(
|
||||
private val items: List<DrawerMenuItem>,
|
||||
private val onItemClick: (DrawerMenuItem) -> Unit
|
||||
) : RecyclerView.Adapter<DrawerSubmenuAdapter.ViewHolder>() {
|
||||
|
||||
data class DrawerMenuItem(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: Int? = null,
|
||||
val isSelected: Boolean = false
|
||||
)
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.itemIcon)
|
||||
val title: TextView = itemView.findViewById(R.id.itemTitle)
|
||||
val check: ImageView = itemView.findViewById(R.id.itemCheck)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_drawer, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
holder.title.text = item.title
|
||||
|
||||
if (item.icon != null) {
|
||||
holder.icon.setImageResource(item.icon)
|
||||
holder.icon.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.icon.visibility = View.GONE
|
||||
}
|
||||
|
||||
holder.check.visibility = if (item.isSelected) View.VISIBLE else View.GONE
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,6 @@ import android.content.Context
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
|
|
@ -47,11 +46,6 @@ class MessageAdapter(private val messages: List<Message>) : RecyclerView.Adapter
|
|||
class UserMessageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
fun bind(message: Message) {
|
||||
val textView = itemView.findViewById<TextView>(R.id.messageText)
|
||||
val senderNameView = itemView.findViewById<TextView>(R.id.senderName)
|
||||
val senderIconView = itemView.findViewById<ImageView>(R.id.senderIcon)
|
||||
|
||||
senderIconView.visibility = View.VISIBLE
|
||||
senderNameView.text = message.senderName ?: "Вы"
|
||||
textView.text = message.content
|
||||
textView.setBackgroundResource(R.drawable.bg_message_user)
|
||||
textView.setTextColor(ContextCompat.getColor(itemView.context, R.color.user_message_text))
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package com.mistral.chat.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mistral.chat.R
|
||||
import com.mistral.chat.data.Profile
|
||||
|
||||
class ProfilesAdapter(
|
||||
private val profiles: List<Profile>,
|
||||
private val onProfileClick: (Profile) -> Unit,
|
||||
private val onProfileLongClick: (Profile) -> Unit,
|
||||
private val getSelectedProfileId: () -> Long?
|
||||
) : RecyclerView.Adapter<ProfilesAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val icon: ImageView = view.findViewById(R.id.profileIcon)
|
||||
val name: TextView = view.findViewById(R.id.profileName)
|
||||
val checkmark: ImageView = view.findViewById(R.id.profileCheckmark)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_profile, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val profile = profiles[position]
|
||||
val selectedId = getSelectedProfileId()
|
||||
|
||||
holder.name.text = if (profile.name.length > 12) {
|
||||
profile.name.take(12) + "..."
|
||||
} else {
|
||||
profile.name
|
||||
}
|
||||
holder.name.alpha = if (profile.id == selectedId) 1.0f else 0.7f
|
||||
holder.checkmark.visibility = if (profile.id == selectedId) View.VISIBLE else View.GONE
|
||||
|
||||
holder.itemView.setOnClickListener { onProfileClick(profile) }
|
||||
holder.itemView.setOnLongClickListener {
|
||||
onProfileLongClick(profile)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = profiles.size
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package com.mistral.chat.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mistral.chat.R
|
||||
import com.mistral.chat.data.Session
|
||||
|
||||
class SessionsAdapter(
|
||||
private val sessions: List<Session>,
|
||||
private val getCurrentSessionId: () -> Long?,
|
||||
private val onSessionClick: (Session) -> Unit,
|
||||
private val onSessionLongClick: (Session) -> Unit
|
||||
) : RecyclerView.Adapter<SessionsAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val title: TextView = view.findViewById(R.id.sessionTitle)
|
||||
val checkmark: ImageView = view.findViewById(R.id.sessionCheckmark)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_session, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val session = sessions[position]
|
||||
val currentId = getCurrentSessionId()
|
||||
holder.title.text = session.title
|
||||
holder.title.alpha = if (session.id == currentId) 1.0f else 0.7f
|
||||
holder.checkmark.visibility = if (session.id == currentId) View.VISIBLE else View.GONE
|
||||
|
||||
holder.itemView.setOnClickListener { onSessionClick(session) }
|
||||
holder.itemView.setOnLongClickListener {
|
||||
onSessionLongClick(session)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = sessions.size
|
||||
}
|
||||
|
|
@ -2,5 +2,5 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#00000000"/>
|
||||
<corners android:radius="12dp"/>
|
||||
<corners android:radius="20dp"/>
|
||||
</shape>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M3,18h18v-2H3v2zm0,-5h18v-2H3v2zm0,-7v2h18V6H3z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zm0,2c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94c0,-0.31 -0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.37 4.8,11.69 4.8,12s0.02,0.63 0.06,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
|
|
@ -1,18 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -24,19 +19,9 @@
|
|||
android:layout_height="?attr/actionBarSize"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/hamburgerButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_menu_hamburger"
|
||||
android:contentDescription="@string/settings"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logoButton"
|
||||
android:layout_width="36dp"
|
||||
|
|
@ -101,16 +86,16 @@
|
|||
android:id="@+id/inputCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
app:cardCornerRadius="28dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
|
|
@ -122,11 +107,11 @@
|
|||
android:background="@android:color/transparent"
|
||||
android:hint="@string/enter_message"
|
||||
android:imeOptions="actionSend"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="5"
|
||||
android:minHeight="56dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
|
|
@ -134,7 +119,6 @@
|
|||
android:id="@+id/sendButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_mistral_logo"
|
||||
android:background="@drawable/bg_send_button"
|
||||
|
|
@ -148,26 +132,4 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/navigationView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/nav_header"
|
||||
app:menu="@menu/drawer_menu" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rightPanelContainer"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<include layout="@layout/panel_right" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
</RelativeLayout>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/apiKeyLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/api_key_hint"
|
||||
app:endIconMode="password_toggle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/apiKeyInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:digits="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
android:maxLength="64"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clear_all_confirm"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/deleteProfilesCheckbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clear_all_delete_profiles"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
android:id="@+id/nameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName|textCapSentences"
|
||||
android:inputType="textPersonName"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
android:id="@+id/bioInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="3"
|
||||
android:maxLines="5" />
|
||||
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
android:id="@+id/preferencesInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2"
|
||||
android:maxLines="4" />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_name"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/profileNameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/profile_bio"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/profileBioInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/profile_preferences"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/profilePreferencesInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profilesList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="300dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/addProfileButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/new_profile"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnProfiles"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profiles"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:icon="@drawable/ic_person"
|
||||
app:iconGravity="start"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:icon="@drawable/ic_settings"
|
||||
app:iconGravity="start"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnClearHistory"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clear_history"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:iconGravity="start"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnApiKey"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/api_key"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:icon="@drawable/ic_key"
|
||||
app:iconGravity="start"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAbout"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/about"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:icon="@drawable/ic_info"
|
||||
app:iconGravity="start"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_name"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/nameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/profile_bio"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bioInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/profile_preferences"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/preferencesInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancelButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/save" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/deleteButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/delete"
|
||||
android:textColor="?attr/colorError"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBack"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAppearance"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/appearance_settings"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSession"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/session_settings"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/drawerSubmenuContainer"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
android:contentDescription="@string/back" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/submenuTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/submenuRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_person"
|
||||
app:tint="?attr/colorOnSurface"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="16dp"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemCheck"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_check"
|
||||
android:visibility="gone"
|
||||
app:tint="?attr/colorPrimary"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
android:id="@+id/messageText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="350dp"
|
||||
android:maxWidth="280dp"
|
||||
android:padding="12dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
|||
|
|
@ -6,34 +6,14 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/senderName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
app:layout_constraintEnd_toStartOf="@id/senderIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/senderIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/ic_person"
|
||||
android:contentDescription="@string/profile"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="350dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:maxWidth="280dp"
|
||||
android:padding="12dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/senderIcon" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profileIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_person"
|
||||
android:contentDescription="@string/profile" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profileName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profileCheckmark"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/ic_check"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/selected" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sessionCheckmark"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/ic_check"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/save" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:background="?attr/colorPrimaryContainer">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/panelRightContent"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/profiles"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profilesRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/sessions"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sessionsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/newSessionButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/new_session"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currentKeyText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/changeKeyButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Изменить ключ" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/themeSystemButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/system_theme" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/themeLightButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/light_theme" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/themeDarkButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/dark_theme" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/screenBaseContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/screenBackButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
android:contentDescription="@string/back" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screenTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/screenContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clear_history_message"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clearSessionsButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Удалить сессии" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clearAllButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Удалить всё (включая профили)"
|
||||
android:textColor="?attr/colorError" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_name"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/profileNameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/profile_bio"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/profileBioInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/profile_preferences"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/profilePreferencesInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/saveProfileButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/save" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/deleteProfileButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/delete"
|
||||
android:textColor="?attr/colorError"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profilesList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/addProfileButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/new_profile"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sessionLastButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/open_last_session" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sessionNewButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/start_new_session" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_profiles"
|
||||
android:title="@string/manage_profiles"
|
||||
android:icon="@drawable/ic_person" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_appearance"
|
||||
android:title="@string/appearance_settings"
|
||||
android:icon="@drawable/ic_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_session"
|
||||
android:title="@string/session_settings"
|
||||
android:icon="@drawable/ic_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_all"
|
||||
android:title="@string/clear_history"
|
||||
android:icon="@drawable/ic_delete" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_api_key"
|
||||
android:title="@string/api_key"
|
||||
android:icon="@drawable/ic_key" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:title="@string/about"
|
||||
android:icon="@drawable/ic_info" />
|
||||
|
||||
</menu>
|
||||
|
|
@ -2,10 +2,6 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_api_key"
|
||||
android:title="@string/api_key" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_profile"
|
||||
android:title="@string/profile" />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@
|
|||
|
||||
<style name="AlertDialogShapeAppearance" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">28dp</item>
|
||||
<item name="cornerSize">24dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -11,69 +11,6 @@
|
|||
<string name="profile_name">Имя</string>
|
||||
<string name="profile_bio">О себе</string>
|
||||
<string name="profile_preferences">Предпочтения</string>
|
||||
<string name="profile_system_prompt">Системный промт</string>
|
||||
<string name="profile_system_prompt_hint">Инструкции для AI (до 4000 символов). Этот текст будет добавлен как system message.</string>
|
||||
<string name="profile_system_prompt_default">Ты - AI ассистент с долговременной памятью и доступом к интернету.
|
||||
|
||||
=== ТЕКУЩАЯ ДАТА ===
|
||||
При старте сессии ты получаешь актуальную дату автоматически. Используй эту информацию для ответов.
|
||||
ВНИМАНИЕ: Сейчас {CURRENT_YEAR} год. Если пользователь спрашивает про свежие новости - проверяй даты ВСЕХ событий. Не используй старые статьи (прошлого года и ранее) как "свежие новости".
|
||||
|
||||
=== ТВОИ ВОЗМОЖНОСТИ ===
|
||||
|
||||
У тебя есть инструменты:
|
||||
1. open_url - получить текст с веб-страницы (ОСНОВНОЙ для новостей и статей)
|
||||
2. web_search - поиск в Wikipedia (для фактов, справок, НЕ для новостей)
|
||||
3. get_local_time - текущее время
|
||||
4. get_date - текущая дата
|
||||
5. get_weather - погода
|
||||
6. send_notification - уведомление
|
||||
7. memory_* - управление памятью
|
||||
|
||||
=== ПРИОРИТЕТЫ ===
|
||||
|
||||
1. Свежие новости → open_url → RSS-ленты (самый эффективный способ):
|
||||
- lenta.ru → https://lenta.ru/rss/
|
||||
- kommersant.ru → https://www.kommersant.ru/rss/news.xml
|
||||
- ria.ru → страница (нет RSS)
|
||||
2. Подробности о новости → open_url → URL статьи (если знаешь URL или видишь в тексте)
|
||||
3. Факты, справки, "что такое..." → web_search → Wikipedia
|
||||
4. Погода → get_weather
|
||||
5. Дата/время → get_date / get_local_time
|
||||
|
||||
=== ПРАВИЛА ===
|
||||
|
||||
1. Сейчас {CURRENT_YEAR} год - используй для проверки актуальности
|
||||
2. Новости - ИСПОЛЬЗУЙ RSS для свежих новостей, это самый быстрый способ
|
||||
3. Для новостей открывай RSS-ленты, они дают чистые заголовки с датами и ссылками
|
||||
4. Для статьи подробнее - используй open_url с URL этой статьи
|
||||
5. Факты/справки - web_search (Wikipedia)
|
||||
6. Если не можешь найти актуальную информацию - скажи "не могу найти свежие данные"
|
||||
7. Не придумывай даты - проверяй через open_url на сайте
|
||||
8. Для предпочтений пользователя - memory_preference
|
||||
9. Для важных фактов о пользователе - memory_store
|
||||
10. Для выводов из поведения - memory_learn
|
||||
11. При подтверждении твоего вывода - memory_reinforce
|
||||
12. ЕСЛИ в памяти есть информация - используй когда нет результатов поиска
|
||||
13. "Что ты обо мне знаешь?" - выведи ВСЕ факты И предпочтения
|
||||
14. НЕ копируй целиком - перескажи своими словами
|
||||
|
||||
=== ПРИМЕРЫ ===
|
||||
|
||||
- "Что в новостях?" → open_url → https://lenta.ru/rss/ (или kommersant.ru/rss/news.xml)
|
||||
- "Что-то про спорт?" → open_url → lenta.ru/rss/ → выбери категорию "Спорт"
|
||||
- "Подробнее про [заголовок]" → open_url → URL статьи из RSS
|
||||
- "Что такое X?" → web_search → Wikipedia
|
||||
- "Какая погода в Москве?" → get_weather
|
||||
- "Какое сегодня число?" → get_date
|
||||
- "Предпочитаю новости киберспорта" → memory_preference(key="интересы", value="киберспорт")
|
||||
- "Что ты обо мне знаешь?" → выведи всю память
|
||||
- "Подробнее про [заголовок]" → open_url → URL статьи (если знаешь или видишь в тексте)
|
||||
- "Что такое X?" → web_search → Wikipedia
|
||||
- "Какая погода в Москве?" → get_weather
|
||||
- "Какое сегодня число?" → get_date
|
||||
- "Предпочитаю новости киберспорта" → memory_preference(key="интересы", value="киберспорт")
|
||||
- "Что ты обо мне знаешь?" → выведи всю память</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="delete">Удалить</string>
|
||||
|
|
@ -88,44 +25,4 @@
|
|||
<string name="bio_hint">Расскажите о себе...</string>
|
||||
<string name="no_profile">Профиль не установлен</string>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="api_key">Mistral API</string>
|
||||
<string name="api_key_title">Mistral API ключ</string>
|
||||
<string name="api_key_hint">Введите API ключ</string>
|
||||
<string name="api_key_saved">API ключ сохранён</string>
|
||||
<string name="api_key_deleted">API ключ удалён</string>
|
||||
<string name="no_api_key">API ключ не установлен</string>
|
||||
<string name="api_key_current">Текущий ключ: %s</string>
|
||||
<string name="enter_api_key">Введите API ключ</string>
|
||||
<string name="api_key_required">Требуется API ключ Mistral</string>
|
||||
<string name="clear_all_history">Очистить всю историю</string>
|
||||
<string name="clear_all_confirm">Удалить все сессии и сообщения?</string>
|
||||
<string name="history_cleared">История очищена</string>
|
||||
<string name="sessions">Сессии</string>
|
||||
<string name="new_session">Новая сессия</string>
|
||||
<string name="no_sessions">Нет сессий</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="profiles">Профили</string>
|
||||
<string name="manage_profiles">Управление профилями</string>
|
||||
<string name="profiles_uppercase">ПРОФИЛИ</string>
|
||||
<string name="new_profile">Новый профиль</string>
|
||||
<string name="edit">Редактировать</string>
|
||||
<string name="selected">Выбрано</string>
|
||||
<string name="clear_all_delete_profiles">Удалить все профили</string>
|
||||
<string name="profile_info">Профиль: %s</string>
|
||||
<string name="appearance_settings">Внешний вид</string>
|
||||
<string name="session_settings">Сессия при запуске</string>
|
||||
<string name="location_settings">Местоположение</string>
|
||||
<string name="location_title">Настройки местоположения</string>
|
||||
<string name="timezone_label">Часовой пояс</string>
|
||||
<string name="city_label">Город по умолчанию</string>
|
||||
<string name="city_hint">Например: Москва, Санкт-Петербург</string>
|
||||
<string name="location_saved">Настройки сохранены</string>
|
||||
<string name="clear_history">Очистить историю</string>
|
||||
<string name="clear_history_message">Удалить все сессии и сообщения?</string>
|
||||
<string name="open_last_session">Открывать последнюю сессию</string>
|
||||
<string name="start_new_session">Начинать новую сессию</string>
|
||||
<string name="light_theme">Светлая</string>
|
||||
<string name="dark_theme">Тёмная</string>
|
||||
<string name="back">Назад</string>
|
||||
<string name="system_theme">Системная</string>
|
||||
</resources>
|
||||
|
|
@ -29,6 +29,6 @@
|
|||
|
||||
<style name="AlertDialogShapeAppearance" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">28dp</item>
|
||||
<item name="cornerSize">24dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// Top-level build file
|
||||
plugins {
|
||||
id 'com.android.application' version '8.2.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
|
||||
id 'com.google.devtools.ksp' version '1.9.22-1.0.17' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue