diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/AppSettingsStore.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/AppSettingsStore.kt index d6dd820..5d1a16d 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/AppSettingsStore.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/AppSettingsStore.kt @@ -1,6 +1,7 @@ package com.example.local_llm import android.content.Context +import androidx.annotation.ColorRes import androidx.annotation.StringRes enum class AppAppearanceMode { @@ -11,14 +12,16 @@ enum class AppAppearanceMode { enum class AppAccentOption( val darkStyleRes: Int, val lightStyleRes: Int, - @field:StringRes val labelResId: Int + @field:StringRes val labelResId: Int, + @field:ColorRes val swatchColorRes: Int ) { - OCEAN(R.style.Theme_local_llm, R.style.Theme_local_llm_Light, R.string.accent_blue), - MIDNIGHT(R.style.Theme_local_llm_Midnight, R.style.Theme_local_llm_Midnight_Light, R.string.accent_indigo), - FOREST(R.style.Theme_local_llm_Forest, R.style.Theme_local_llm_Forest_Light, R.string.accent_green), - VIOLET(R.style.Theme_local_llm_Violet, R.style.Theme_local_llm_Violet_Light, R.string.accent_violet), - AMBER(R.style.Theme_local_llm_Amber, R.style.Theme_local_llm_Amber_Light, R.string.accent_amber), - CORAL(R.style.Theme_local_llm_Coral, R.style.Theme_local_llm_Coral_Light, R.string.accent_coral); + POCKET(R.style.Theme_local_llm, R.style.Theme_local_llm_Light, R.string.accent_pocket, R.color.send_button_fill), + OCEAN(R.style.Theme_local_llm_Ocean, R.style.Theme_local_llm_Ocean_Light, R.string.accent_blue, R.color.ocean_send_fill), + MIDNIGHT(R.style.Theme_local_llm_Midnight, R.style.Theme_local_llm_Midnight_Light, R.string.accent_indigo, R.color.midnight_send_fill), + FOREST(R.style.Theme_local_llm_Forest, R.style.Theme_local_llm_Forest_Light, R.string.accent_green, R.color.forest_send_fill), + VIOLET(R.style.Theme_local_llm_Violet, R.style.Theme_local_llm_Violet_Light, R.string.accent_violet, R.color.violet_send_fill), + AMBER(R.style.Theme_local_llm_Amber, R.style.Theme_local_llm_Amber_Light, R.string.accent_amber, R.color.amber_send_fill), + CORAL(R.style.Theme_local_llm_Coral, R.style.Theme_local_llm_Coral_Light, R.string.accent_coral, R.color.coral_send_fill); fun styleFor(appearance: AppAppearanceMode): Int { return when (appearance) { @@ -31,14 +34,14 @@ enum class AppAccentOption( fun fromStoredName(name: String?): AppAccentOption { return when (name) { "TEAL" -> AMBER - else -> entries.firstOrNull { it.name == name } ?: OCEAN + else -> entries.firstOrNull { it.name == name } ?: POCKET } } } } data class AppSettings( - val accent: AppAccentOption = AppAccentOption.OCEAN, + val accent: AppAccentOption = AppAccentOption.POCKET, val appearance: AppAppearanceMode = AppAppearanceMode.DARK, val chatFontSizeSp: Float = 16f ) @@ -56,7 +59,7 @@ class AppSettingsStore(context: Context) { fun load(): AppSettings { val accent = AppAccentOption.fromStoredName( - prefs.getString(KEY_ACCENT, AppAccentOption.OCEAN.name) + prefs.getString(KEY_ACCENT, AppAccentOption.POCKET.name) ) val appearance = runCatching { AppAppearanceMode.valueOf(prefs.getString(KEY_APPEARANCE, AppAppearanceMode.DARK.name)!!) diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatAdapter.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatAdapter.kt index acee1c0..3868883 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatAdapter.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatAdapter.kt @@ -87,6 +87,12 @@ class ChatAdapter( holder.textView.setBackgroundResource(bubbleBackground) holder.messageImage.setBackgroundResource(bubbleBackground) holder.textView.setTextColor(resolveThemeColor(holder.textView, textColorAttr)) + val tokensPerSecond = turn.tokensPerSecond + ?.takeIf { !turn.isUser && !turn.isStreaming && !turn.isImage && !turn.isAttachment } + holder.speedView.visibility = if (tokensPerSecond == null) View.GONE else View.VISIBLE + holder.speedView.text = tokensPerSecond?.let { + holder.speedView.context.getString(R.string.tokens_per_second_format, it) + } holder.textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp) holder.preResponseStatusView.setTextColor(resolveThemeColor(holder.preResponseStatusView, R.attr.colorStatusText)) holder.preResponseStatusView.setTextSize(TypedValue.COMPLEX_UNIT_SP, (fontSizeSp - 2f).coerceAtLeast(12f)) @@ -447,6 +453,7 @@ class ChatAdapter( val messageImage: ImageView = view.findViewById(R.id.messageImage) val textView: TextView = view.findViewById(R.id.messageText) val copyButton: ImageButton = view.findViewById(R.id.copyResponseButton) + val speedView: TextView = view.findViewById(R.id.messageSpeed) var messageTextRenderKey: TextRenderKey? = null var thoughtTextRenderKey: TextRenderKey? = null diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatSessionStore.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatSessionStore.kt index e34f267..b82a9a6 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatSessionStore.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatSessionStore.kt @@ -123,6 +123,7 @@ class ChatSessionStore(context: Context) { } turn.thinkingText?.let { put("thinkingText", it) } turn.thinkingDurationMillis?.let { put("thinkingDurationMillis", it) } + turn.tokensPerSecond?.let { put("tokensPerSecond", it.toDouble()) } put("stopped", turn.stopped) put("renderAsMarkdown", turn.renderAsMarkdown) put("isStreaming", turn.isStreaming) @@ -156,6 +157,9 @@ class ChatSessionStore(context: Context) { thinkingText = turnJson.optString("thinkingText").takeIf { it.isNotBlank() }, thinkingDurationMillis = turnJson.optLong("thinkingDurationMillis") .takeIf { turnJson.has("thinkingDurationMillis") }, + tokensPerSecond = turnJson.optDouble("tokensPerSecond") + .takeIf { turnJson.has("tokensPerSecond") } + ?.toFloat(), stopped = turnJson.optBoolean("stopped"), renderAsMarkdown = turnJson.optBoolean("renderAsMarkdown", true), isStreaming = turnJson.optBoolean("isStreaming", false), diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatTurn.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatTurn.kt index a6d66fd..b844b52 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatTurn.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatTurn.kt @@ -21,6 +21,7 @@ data class ChatTurn( val preResponseStatusText: String? = null, val thinkingText: String? = null, val thinkingDurationMillis: Long? = null, + val tokensPerSecond: Float? = null, val stopped: Boolean = false, val renderAsMarkdown: Boolean = false, val isStreaming: Boolean = false, @@ -58,6 +59,7 @@ fun ChatTurn.asModelMemoryTurn(): ChatTurn? { preResponseStatusText = null, thinkingText = null, thinkingDurationMillis = null, + tokensPerSecond = null, stopped = false, renderAsMarkdown = false, isStreaming = false, diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatUiState.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatUiState.kt index 2f99e68..e707216 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatUiState.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/ChatUiState.kt @@ -16,5 +16,14 @@ data class ChatUiState( val activeAttachmentId: String? = null, /** Estimated tokens the chat already takes in the context window; 0 when unknown. */ val contextUsedTokens: Int = 0, - val contextWindowTokens: Int = 0 + val contextWindowTokens: Int = 0, + /** How [contextUsedTokens] splits into its parts; parts add up to it. */ + val contextBreakdown: ContextBreakdown = ContextBreakdown() +) + +data class ContextBreakdown( + val instructionTokens: Int = 0, + val summaryTokens: Int = 0, + val documentTokens: Int = 0, + val conversationTokens: Int = 0 ) diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/LanServerService.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/LanServerService.kt index c36b5a4..d7b3b37 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/LanServerService.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/LanServerService.kt @@ -10,6 +10,7 @@ import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder +import android.net.wifi.WifiManager import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat @@ -60,6 +61,7 @@ class LanServerService : Service() { private var preserveFailureState = false private var stopRequested = false private var serverWakeLock: PowerManager.WakeLock? = null + private var serverWifiLock: WifiManager.WifiLock? = null private lateinit var notificationManager: NotificationManager override fun onCreate() { @@ -226,11 +228,26 @@ class LanServerService : Service() { // awake until Stop, startup failure, or service destruction; never the display. acquire() } + if (serverWifiLock?.isHeld != true) { + // Keeps Wi-Fi from dozing while the screen is off, so browsers on the network can still reach the server. + @Suppress("DEPRECATION") + val mode = WifiManager.WIFI_MODE_FULL_HIGH_PERF + serverWifiLock = runCatching { + applicationContext.getSystemService(WifiManager::class.java) + ?.createWifiLock(mode, "$packageName:LanServer") + ?.apply { + setReferenceCounted(false) + acquire() + } + }.getOrNull() + } } private fun releaseServerWakeLock() { serverWakeLock?.takeIf { it.isHeld }?.release() serverWakeLock = null + serverWifiLock?.takeIf { it.isHeld }?.release() + serverWifiLock = null } private fun startForegroundCompat() { diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/PersistentChatController.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/PersistentChatController.kt index 0e4bab3..a6a0eaa 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/PersistentChatController.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/PersistentChatController.kt @@ -33,6 +33,7 @@ class PersistentChatController( ) { companion object { + private const val MIN_SPEED_SAMPLE_MILLIS = 300L // Roughly 10 tokens worth of text before live markdown rendering kicks in. private const val MARKDOWN_STREAM_CHAR_THRESHOLD = 40 private const val TABLE_MARKDOWN_UPDATE_WORD_STEP = 50 @@ -83,6 +84,7 @@ class PersistentChatController( private var thinkingEnabled = false private var contextUsageKey: List? = null private var contextUsageTokens = 0 + private var contextBreakdown = ContextBreakdown() private var liveMarkdownEnabled = false private var currentGenerationId: Long = 0L private var currentSessionId: String? = null @@ -93,6 +95,7 @@ class PersistentChatController( private var lastPublishedMarkdownTextLength: Int = 0 private var lastPublishedMarkdownAtMillis: Long = 0L private var currentGenerationStartedAtMillis: Long? = null + private var firstOutputAtMillis: Long? = null private var currentThinkingStartedAtMillis: Long? = null private var currentThinkingFinishedAtMillis: Long? = null private var currentGenerationImageFilePaths: List = emptyList() @@ -594,6 +597,7 @@ class PersistentChatController( text = response.text, thinkingText = finalThinkingText, thinkingDurationMillis = thinkingDurationMillis(finalThinkingText), + tokensPerSecond = tokensPerSecond(response.text, finalThinkingText), stopped = false, renderAsMarkdown = true, isStreaming = false @@ -1032,6 +1036,7 @@ class PersistentChatController( if (partialTurn != null && (partialTurn.text.isNotBlank() || !partialTurn.thinkingText.isNullOrBlank())) { committedTurns += partialTurn.copy( thinkingDurationMillis = thinkingDurationMillis(partialTurn.thinkingText), + tokensPerSecond = tokensPerSecond(partialTurn.text, partialTurn.thinkingText), stopped = true, renderAsMarkdown = true, isStreaming = false @@ -1049,12 +1054,16 @@ class PersistentChatController( private fun startGenerationTimer() { val now = SystemClock.elapsedRealtime() currentGenerationStartedAtMillis = now + firstOutputAtMillis = null currentThinkingStartedAtMillis = null currentThinkingFinishedAtMillis = null } private fun updateThinkingTimer(response: BackendResponse) { val now = SystemClock.elapsedRealtime() + if (firstOutputAtMillis == null && (response.text.isNotBlank() || !response.thinkingText.isNullOrBlank())) { + firstOutputAtMillis = now + } if (!response.thinkingText.isNullOrBlank() && currentThinkingStartedAtMillis == null) { currentThinkingStartedAtMillis = currentGenerationStartedAtMillis ?: now } @@ -1073,7 +1082,19 @@ class PersistentChatController( return (end - start).coerceAtLeast(0L) } + /** Decode speed from the first streamed output, using the same estimate as prompt sizing. */ + private fun tokensPerSecond(text: String, thinkingText: String?): Float? { + val start = firstOutputAtMillis ?: return null + val elapsedMillis = SystemClock.elapsedRealtime() - start + if (elapsedMillis < MIN_SPEED_SAMPLE_MILLIS) return null + val tokens = PromptTokenEstimator.estimate(text) + + (thinkingText?.let(PromptTokenEstimator::estimate) ?: 0) + if (tokens <= 0) return null + return tokens * 1000f / elapsedMillis + } + private fun resetGenerationTimer() { + firstOutputAtMillis = null currentGenerationStartedAtMillis = null currentThinkingStartedAtMillis = null currentThinkingFinishedAtMillis = null @@ -1111,11 +1132,51 @@ class PersistentChatController( InferenceRequest(history = modelHistory(), thinkingEnabled = thinkingEnabled, modelInstruction = instruction) ) }.getOrDefault(0) + contextBreakdown = runCatching { contextBreakdown(instruction, contextUsageTokens) } + .getOrDefault(ContextBreakdown(conversationTokens = contextUsageTokens)) contextUsageKey = key } return contextUsageTokens } + /** + * Splits the prompt estimate into the instruction, the summary of earlier messages, document + * passages added to user messages, and the conversation itself. The conversation takes the + * remainder so the parts always add up to [total]. + */ + private fun contextBreakdown(instruction: String, total: Int): ContextBreakdown { + if (total <= 0) return ContextBreakdown() + val baseInstruction = currentModelInstruction() + val instructionTokens = if (baseInstruction.isBlank()) 0 else estimateInstructionTokens(baseInstruction) + val withSummaryTokens = if (instruction == baseInstruction) { + instructionTokens + } else { + estimateInstructionTokens(instruction) + } + val fixedTokens = withSummaryTokens.coerceAtMost(total) + val originals = committedTurns.associateBy { it.id } + var documentTokens = 0 + modelHistory().forEach { turn -> + val typed = originals[turn.id]?.displayText ?: return@forEach + if (turn.role == ChatRole.USER && typed.length < turn.text.length) { + documentTokens += (estimateTurnTokens(turn) - estimateTurnTokens(turn.copy(text = typed))) + .coerceAtLeast(0) + } + } + documentTokens = documentTokens.coerceAtMost(total - fixedTokens) + return ContextBreakdown( + instructionTokens = instructionTokens.coerceAtMost(fixedTokens), + summaryTokens = (fixedTokens - instructionTokens).coerceAtLeast(0), + documentTokens = documentTokens, + conversationTokens = total - fixedTokens - documentTokens + ) + } + + private fun estimateInstructionTokens(instruction: String): Int = + backend.estimateSerializedPromptTokens( + InferenceRequest(history = emptyList(), thinkingEnabled = false, modelInstruction = instruction) + ) + private fun activeSummary(): String? = compaction?.takeIf { ChatCompactionPlanner.isActive(committedTurns.asModelMemoryTurns(), it) }?.summary @@ -1286,7 +1347,8 @@ class PersistentChatController( supportsNativeAudioInput = backend.supportsNativeAudioInput, activeAttachmentId = activeAttachmentId, contextUsedTokens = if (isReady) contextUsage() else 0, - contextWindowTokens = if (isReady) backend.capabilities.contextWindowTokens else 0 + contextWindowTokens = if (isReady) backend.capabilities.contextWindowTokens else 0, + contextBreakdown = if (isReady) contextBreakdown else ContextBreakdown() ) } diff --git a/pocket_llm_src/app/src/main/java/com/example/local_llm/PocketChatActivity.kt b/pocket_llm_src/app/src/main/java/com/example/local_llm/PocketChatActivity.kt index 216d4e4..b2c2956 100644 --- a/pocket_llm_src/app/src/main/java/com/example/local_llm/PocketChatActivity.kt +++ b/pocket_llm_src/app/src/main/java/com/example/local_llm/PocketChatActivity.kt @@ -11,9 +11,9 @@ import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color import android.graphics.ImageDecoder -import android.graphics.Paint import android.graphics.Rect import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable import android.media.MediaRecorder import android.net.Uri import android.os.Build @@ -22,6 +22,10 @@ import android.os.Handler import android.os.Looper import android.os.SystemClock import android.text.Editable +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.text.style.RelativeSizeSpan import android.text.TextWatcher import android.text.method.LinkMovementMethod import android.text.method.PasswordTransformationMethod @@ -31,11 +35,10 @@ import android.text.util.Linkify import android.util.Log import android.util.TypedValue import android.view.Gravity +import android.view.WindowManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import android.widget.Button import android.widget.CheckBox import android.widget.EditText @@ -54,7 +57,6 @@ import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.AppCompatSpinner import androidx.camera.core.CameraSelector import androidx.camera.core.Camera import androidx.camera.core.ImageCapture @@ -74,6 +76,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator @@ -214,6 +218,7 @@ open class PocketChatActivity : AppCompatActivity() { private lateinit var selectedModelLabel: TextView private lateinit var thinkingToggleContainer: View private lateinit var thinkingToggle: CheckBox + private var currentModelSupportsThinking = false private lateinit var newChatButton: View private lateinit var sendButton: Button private lateinit var contextUsageIndicator: CircularProgressIndicator @@ -374,16 +379,7 @@ open class PocketChatActivity : AppCompatActivity() { contextUsageIndicator.setOnClickListener { val state = chatController?.state?.value ?: return@setOnClickListener if (state.contextWindowTokens <= 0) return@setOnClickListener - val message = if (currentModel is OnnxQwenSpec) { - R.string.context_usage_message_trimmed - } else { - R.string.context_usage_message - } - MaterialAlertDialogBuilder(this) - .setTitle(R.string.context_usage_description) - .setMessage(getString(message, state.contextUsedTokens, state.contextWindowTokens, contextUsagePercent(state))) - .setPositiveButton(android.R.string.ok, null) - .show() + showContextUsageDialog(state) } stopButton = findViewById(R.id.stopButton) micInputButton = findViewById(R.id.micInputButton) @@ -441,6 +437,7 @@ open class PocketChatActivity : AppCompatActivity() { } newChatButton.setOnClickListener { + drawerLayout.closeDrawer(GravityCompat.START) startNewChatFromUi() } @@ -477,6 +474,7 @@ open class PocketChatActivity : AppCompatActivity() { thinkingToggle.setOnCheckedChangeListener { _, isChecked -> chatController?.setThinkingEnabled(isChecked) + updateModelSelectorLabel() } val restoredModel = retainedState.modelId?.let(ModelRegistry::findById) @@ -537,6 +535,20 @@ open class PocketChatActivity : AppCompatActivity() { } } + override fun onResume() { + super.onResume() + updateKeepScreenOn() + } + + /** Keeps the screen on while the context test or the LAN server runs, so the phone doesn't lock mid-way. */ + private fun updateKeepScreenOn() { + if (contextProbeRunner != null || LanServerService.isRunning()) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + override fun onStop() { pendingDictationRecordingStart = false discardDictationRecording() @@ -2880,7 +2892,7 @@ open class PocketChatActivity : AppCompatActivity() { if (compactReadyAudio) R.drawable.bg_active_audio_attachment else R.drawable.bg_image_input_chip ) activeAttachmentText.setTextColor( - if (compactReadyAudio) ContextCompat.getColor(this, R.color.white) else resolveThemeColor(R.attr.colorInputText) + if (compactReadyAudio) ContextCompat.getColor(this, R.color.on_accent) else resolveThemeColor(R.attr.colorInputText) ) activeAttachmentContainer.visibility = if (attachmentImportInProgress || descriptor != null) { View.VISIBLE @@ -3102,12 +3114,6 @@ open class PocketChatActivity : AppCompatActivity() { customModelPickerLauncher.launch(arrayOf("*/*")) } .create() - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE) - ?.setTextColor(resolveThemeColor(R.attr.colorSendFill)) - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) - ?.setTextColor(resolveThemeColor(R.attr.colorStatusText)) - } dialog.show() } @@ -3307,11 +3313,18 @@ open class PocketChatActivity : AppCompatActivity() { renderNoControllerState(getString(R.string.context_probe_model_unloaded), preserveTranscript = true) val padding = (20 * resources.displayMetrics.density).toInt() - val progressText = TextView(this).apply { text = getString(R.string.context_probe_preparing) } + val progressText = TextView(this).apply { + text = getString(R.string.context_probe_preparing) + setTextColor(resolveThemeColor(R.attr.colorAssistantText)) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f) + setLineSpacing(dp(2).toFloat(), 1f) + } val progressBar = LinearProgressIndicator(this).apply { isIndeterminate = true max = 100 setPadding(0, padding / 2, 0, 0) + setIndicatorColor(resolveThemeColor(R.attr.colorSendFill)) + trackColor = resolveThemeColor(R.attr.colorPocketRaised) } val progressView = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL @@ -3351,6 +3364,7 @@ open class PocketChatActivity : AppCompatActivity() { }, onFinished = { result -> contextProbeRunner = null + updateKeepScreenOn() progressDialog.dismiss() if (!isFinishing && !isDestroyed) { showContextProbeResult(descriptor, result) { @@ -3360,6 +3374,7 @@ open class PocketChatActivity : AppCompatActivity() { }, startTokens = ContextMemoryGuard(this).measuredLargest(descriptor.id) ).also { it.start() } + updateKeepScreenOn() } private fun formatContextProbeProgress(size: Int, steps: List, stage: String): String { @@ -3461,7 +3476,10 @@ open class PocketChatActivity : AppCompatActivity() { title = state.title toolbarSubtitleView.text = currentModel?.displayName ?: getString(R.string.model_picker_empty_subtitle) updateModelSelectorLabel() - thinkingToggleContainer.visibility = if (state.supportsThinking) View.VISIBLE else View.GONE + if (currentModelSupportsThinking != state.supportsThinking) { + currentModelSupportsThinking = state.supportsThinking + updateModelSelectorLabel() + } val gemmaDirectAvailable = isGemmaDirectImageInputAvailable() if (!gemmaDirectAvailable && currentImageInputMode == ImageInputMode.GEMMA_DIRECT) { selectOcrImageInputMode(resetGemmaDirectInputs = true) @@ -3504,6 +3522,81 @@ open class PocketChatActivity : AppCompatActivity() { } } + private fun showContextUsageDialog(state: ChatUiState) { + val view = layoutInflater.inflate(R.layout.dialog_context_usage, null) + val window = state.contextWindowTokens + val used = state.contextUsedTokens.coerceIn(0, window) + view.findViewById(R.id.contextUsageUsed).text = getString(R.string.context_usage_tokens, used) + view.findViewById(R.id.contextUsageTotal).text = getString(R.string.context_usage_total, window) + view.findViewById(R.id.contextUsagePercent).text = + getString(R.string.context_usage_percent, contextUsagePercent(state)) + view.findViewById(R.id.contextUsageNote).setText( + if (currentModel is OnnxQwenSpec) R.string.context_usage_note_trimmed else R.string.context_usage_note + ) + + val breakdown = state.contextBreakdown + val parts = listOf( + Triple(R.string.context_usage_instruction, breakdown.instructionTokens, resolveThemeColor(R.attr.colorSendFill)), + Triple(R.string.context_usage_summary, breakdown.summaryTokens, ContextCompat.getColor(this, R.color.context_summary)), + Triple(R.string.context_usage_documents, breakdown.documentTokens, ContextCompat.getColor(this, R.color.context_documents)), + Triple(R.string.context_usage_conversation, breakdown.conversationTokens, ContextCompat.getColor(this, R.color.context_conversation)) + ) + val bar = view.findViewById(R.id.contextUsageBar) + bar.clipToOutline = true + parts.filter { it.second > 0 }.forEach { (_, tokens, color) -> + bar.addView(View(this).apply { + setBackgroundColor(color) + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, tokens.toFloat()) + }) + } + val free = window - used + if (free > 0) { + bar.addView(View(this).apply { + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, free.toFloat()) + }) + } + + val rows = view.findViewById(R.id.contextUsageRows) + parts.filter { it.second > 0 || it.first == R.string.context_usage_conversation } + .forEach { (label, tokens, color) -> rows.addView(contextUsageRow(label, tokens, color)) } + rows.addView(contextUsageRow(R.string.context_usage_free, free, resolveThemeColor(R.attr.colorPocketRaised))) + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.context_usage_description) + .setView(view) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + private fun contextUsageRow(label: Int, tokens: Int, color: Int): View { + return LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(0, dp(6), 0, dp(6)) + addView(View(this@PocketChatActivity).apply { + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(color) + } + layoutParams = LinearLayout.LayoutParams(dp(10), dp(10)) + }) + addView(TextView(this@PocketChatActivity).apply { + text = getString(label) + setTextColor(resolveThemeColor(R.attr.colorAssistantText)) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { + marginStart = dp(12) + } + }) + addView(TextView(this@PocketChatActivity).apply { + text = getString(R.string.context_usage_tokens, tokens) + setTextColor(resolveThemeColor(R.attr.colorStatusText)) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + fontFeatureSettings = "tnum" + }) + } + } + private fun contextUsagePercent(state: ChatUiState): Int = (state.contextUsedTokens.toLong() * 100 / state.contextWindowTokens.coerceAtLeast(1)).toInt().coerceIn(0, 100) @@ -3515,6 +3608,7 @@ open class PocketChatActivity : AppCompatActivity() { toolbarSubtitleView.text = currentModel?.displayName ?: getString(R.string.model_picker_empty_subtitle) updateModelSelectorLabel() thinkingToggleContainer.visibility = View.GONE + currentModelSupportsThinking = false newChatButton.isEnabled = false sendButton.visibility = View.VISIBLE stopButton.visibility = View.GONE @@ -3534,8 +3628,17 @@ open class PocketChatActivity : AppCompatActivity() { private fun updateModelSelectorLabel() { if (!::selectedModelLabel.isInitialized) return - selectedModelLabel.text = currentModel?.displayName - ?: getString(R.string.model_picker_empty_subtitle) + val modelName = currentModel?.displayName ?: getString(R.string.model_picker_empty_subtitle) + selectedModelLabel.text = if (currentModelSupportsThinking && thinkingToggle.isChecked) { + val suffix = getString(R.string.model_chip_thinking_suffix) + SpannableString("$modelName · $suffix").apply { + val start = length - suffix.length + setSpan(ForegroundColorSpan(resolveThemeColor(R.attr.colorSendFill)), start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan(RelativeSizeSpan(0.85f), start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } else { + modelName + } toolbarModelSelector.contentDescription = getString( R.string.model_popup_selected_format, selectedModelLabel.text, @@ -3632,6 +3735,13 @@ open class PocketChatActivity : AppCompatActivity() { ) { handleModelSelection(descriptor) } } } + if (currentModelSupportsThinking) { + addHeading(getString(R.string.model_popup_thinking_heading)) + addOption( + getString(R.string.model_popup_thinking_option), + selected = thinkingToggle.isChecked + ) { thinkingToggle.isChecked = !thinkingToggle.isChecked } + } addHeading(getString(R.string.model_popup_image_heading)) addOption( getString(R.string.ocr_image_model_name), @@ -3653,14 +3763,15 @@ open class PocketChatActivity : AppCompatActivity() { toolbarModelSelector.getLocationOnScreen(anchorLocation) val visibleFrame = Rect() window.decorView.getWindowVisibleDisplayFrame(visibleFrame) - val spaceAbove = (anchorLocation[1] - visibleFrame.top - dp(12)).coerceAtLeast(dp(120)) + val anchorBottom = anchorLocation[1] + toolbarModelSelector.height + val spaceBelow = (visibleFrame.bottom - anchorBottom - dp(12)).coerceAtLeast(dp(120)) val optionsWidth = panelWidth - dp(20) options.measure( View.MeasureSpec.makeMeasureSpec(optionsWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) scroll.layoutParams = scroll.layoutParams.apply { - height = minOf(options.measuredHeight, (spaceAbove - dp(64)).coerceAtLeast(dp(56))) + height = minOf(options.measuredHeight, (spaceBelow - dp(64)).coerceAtLeast(dp(56))) } content.measure( View.MeasureSpec.makeMeasureSpec(panelWidth, View.MeasureSpec.EXACTLY), @@ -3669,7 +3780,7 @@ open class PocketChatActivity : AppCompatActivity() { popup.height = content.measuredHeight val x = (anchorLocation[0] + toolbarModelSelector.width - panelWidth) .coerceIn(dp(12), (resources.displayMetrics.widthPixels - panelWidth - dp(12)).coerceAtLeast(dp(12))) - val y = (anchorLocation[1] - popup.height - dp(6)).coerceAtLeast(visibleFrame.top + dp(4)) + val y = anchorBottom + dp(6) popup.showAtLocation(toolbarModelSelector, Gravity.TOP or Gravity.START, x, y) } @@ -3872,6 +3983,16 @@ open class PocketChatActivity : AppCompatActivity() { isAvailable -> true else -> activeDownloadModelId == null } + styleModelOption( + itemView, + actionButton, + when { + isDownloadingThisModel -> ModelOptionAction.STOP + isCurrentModel -> ModelOptionAction.CURRENT + isAvailable -> ModelOptionAction.USE + else -> ModelOptionAction.DOWNLOAD + } + ) actionButton.setOnClickListener { if (isDownloadingThisModel) { cancelModelDownload() @@ -3895,6 +4016,35 @@ open class PocketChatActivity : AppCompatActivity() { container.addView(itemView) } + private enum class ModelOptionAction { CURRENT, USE, DOWNLOAD, STOP } + + private fun styleModelOption(itemView: View, actionButton: Button, action: ModelOptionAction) { + val button = actionButton as? MaterialButton ?: return + val accent = resolveThemeColor(R.attr.colorSendFill) + val softAccent = resolveThemeColor(R.attr.colorUserBubble) + val text = resolveThemeColor(R.attr.colorAssistantText) + val outline = resolveThemeColor(R.attr.colorPocketOutline) + val transparent = Color.TRANSPARENT + val (fill, stroke, label) = when (action) { + ModelOptionAction.CURRENT -> Triple(accent, accent, ContextCompat.getColor(this, R.color.on_accent)) + ModelOptionAction.USE -> Triple(transparent, outline, text) + ModelOptionAction.DOWNLOAD -> Triple(softAccent, softAccent, accent) + ModelOptionAction.STOP -> Triple( + ContextCompat.getColor(this, R.color.stop_soft_fill), + transparent, + ContextCompat.getColor(this, R.color.stop_button_fill) + ) + } + button.backgroundTintList = ColorStateList.valueOf(fill) + button.strokeColor = ColorStateList.valueOf(stroke) + button.setTextColor(label) + itemView.backgroundTintList = if (action == ModelOptionAction.CURRENT) { + ColorStateList.valueOf(softAccent) + } else { + null + } + } + private fun addOcrImageModelOption( inflater: LayoutInflater, container: LinearLayout @@ -3926,6 +4076,7 @@ open class PocketChatActivity : AppCompatActivity() { getString(R.string.model_action_use) } actionButton.isEnabled = !isCurrent + styleModelOption(itemView, actionButton, if (isCurrent) ModelOptionAction.CURRENT else ModelOptionAction.USE) actionButton.setOnClickListener { handleOcrImageModelSelection() } @@ -3969,6 +4120,7 @@ open class PocketChatActivity : AppCompatActivity() { else -> getString(R.string.model_action_unavailable) } actionButton.isEnabled = isAvailable && !isCurrent + styleModelOption(itemView, actionButton, if (isCurrent) ModelOptionAction.CURRENT else ModelOptionAction.USE) actionButton.setOnClickListener { handleGemmaDirectImageModelSelection() } @@ -4047,11 +4199,13 @@ open class PocketChatActivity : AppCompatActivity() { val statusView: TextView = dialogView.findViewById(R.id.lanServerStatus) val endpointView: TextView = dialogView.findViewById(R.id.lanServerEndpoint) val apiEndpointView: TextView = dialogView.findViewById(R.id.lanServerApiEndpoint) - val copyApiUrlLink: TextView = dialogView.findViewById(R.id.lanServerCopyApiUrlLink) + val statusDot: View = dialogView.findViewById(R.id.lanServerStatusDot) + val statusDetailView: TextView = dialogView.findViewById(R.id.lanServerStatusDetail) + val copyApiUrlLink: View = dialogView.findViewById(R.id.lanServerCopyApiUrlLink) val passwordInput: EditText = dialogView.findViewById(R.id.lanServerPasswordInput) val passwordVisibilityButton: ImageButton = dialogView.findViewById(R.id.lanServerPasswordVisibilityButton) val savePasswordButton: MaterialButton = dialogView.findViewById(R.id.lanServerSavePasswordButton) - val copyUrlLink: TextView = dialogView.findViewById(R.id.lanServerCopyUrlLink) + val copyUrlLink: View = dialogView.findViewById(R.id.lanServerCopyUrlLink) val startButton: MaterialButton = dialogView.findViewById(R.id.lanServerStartButton) val closeButton: MaterialButton = dialogView.findViewById(R.id.lanServerCloseButton) var savedPassword = LanServerStateStore.getPassword(this) @@ -4062,8 +4216,6 @@ open class PocketChatActivity : AppCompatActivity() { } else { getString(R.string.lan_server_password_hint) } - copyUrlLink.paintFlags = copyUrlLink.paintFlags or Paint.UNDERLINE_TEXT_FLAG - copyApiUrlLink.paintFlags = copyApiUrlLink.paintFlags or Paint.UNDERLINE_TEXT_FLAG val dialog = dialogBuilder .setView(dialogView) .create() @@ -4074,6 +4226,7 @@ open class PocketChatActivity : AppCompatActivity() { LanServerStateStore.markStopped(this) } val state = LanServerStateStore.read(this) + updateKeepScreenOn() statusView.text = when (state.status) { LanServerStatus.STOPPED -> getString(R.string.lan_server_status_stopped) LanServerStatus.STARTING -> getString(R.string.lan_server_status_starting) @@ -4081,6 +4234,19 @@ open class PocketChatActivity : AppCompatActivity() { LanServerStatus.FAILED -> getString(R.string.lan_server_status_failed) + ": " + state.errorMessage.orEmpty() } + statusDetailView.text = when (state.status) { + LanServerStatus.RUNNING -> getString(R.string.lan_server_detail_running) + LanServerStatus.STARTING -> getString(R.string.lan_server_starting) + else -> getString(R.string.lan_server_detail_stopped) + } + statusDot.backgroundTintList = ColorStateList.valueOf( + when (state.status) { + LanServerStatus.RUNNING -> ContextCompat.getColor(this, R.color.status_ready_background) + LanServerStatus.STARTING -> ContextCompat.getColor(this, R.color.status_loading_background) + LanServerStatus.FAILED -> ContextCompat.getColor(this, R.color.stop_button_fill) + LanServerStatus.STOPPED -> resolveThemeColor(R.attr.colorStatusText) + } + ) endpointView.text = state.endpoint ?: "—" apiEndpointView.text = state.endpoint?.let(::lanApiBaseUrl) ?: "—" val modelReady = chatController?.state?.value?.isReady == true @@ -4092,6 +4258,8 @@ open class PocketChatActivity : AppCompatActivity() { val webUiAvailable = state.status == LanServerStatus.RUNNING && !state.endpoint.isNullOrBlank() copyUrlLink.isEnabled = webUiAvailable copyApiUrlLink.isEnabled = webUiAvailable + copyUrlLink.alpha = if (webUiAvailable) 1f else 0.4f + copyApiUrlLink.alpha = if (webUiAvailable) 1f else 0.4f passwordInput.isEnabled = !state.isActive savePasswordButton.visibility = if (!state.isActive && passwordInput.text.toString() != savedPassword.orEmpty() @@ -4191,6 +4359,7 @@ open class PocketChatActivity : AppCompatActivity() { closeButton.setOnClickListener { if (LanServerStateStore.read(this).isActive) { LanServerService.stop(this) + window.decorView.postDelayed({ updateKeepScreenOn() }, 1500) } dialog.dismiss() } @@ -4624,28 +4793,18 @@ open class PocketChatActivity : AppCompatActivity() { var draftSettings = previewState?.draftSettings ?: currentSettings val accentOptions = AppAccentOption.entries.toList() val appearanceModeGroup: RadioGroup = dialogView.findViewById(R.id.appearanceModeGroup) - val accentColorSpinner: AppCompatSpinner = dialogView.findViewById(R.id.accentColorSpinner) + val accentSwatchRow: LinearLayout = dialogView.findViewById(R.id.accentSwatchRow) val fontSizeValue: TextView = dialogView.findViewById(R.id.fontSizeValue) val fontSizePreview: TextView = dialogView.findViewById(R.id.fontSizePreview) val fontSizeSeekBar: SeekBar = dialogView.findViewById(R.id.fontSizeSeekBar) val cancelButton: Button = dialogView.findViewById(R.id.settingsCancelButton) val saveButton: Button = dialogView.findViewById(R.id.settingsSaveButton) - val accentAdapter = ArrayAdapter( - dialogContext, - R.layout.item_instruction_preset_spinner, - accentOptions.map { dialogContext.getString(it.labelResId) } - ).apply { - setDropDownViewResource(R.layout.item_instruction_preset_dropdown) - } - accentColorSpinner.adapter = accentAdapter when (draftSettings.appearance) { AppAppearanceMode.LIGHT -> appearanceModeGroup.check(R.id.appearanceLight) AppAppearanceMode.DARK -> appearanceModeGroup.check(R.id.appearanceDark) } - val initialAccentIndex = accentOptions.indexOf(draftSettings.accent).coerceAtLeast(0) - accentColorSpinner.setSelection(initialAccentIndex, false) val initialProgress = (draftSettings.chatFontSizeSp - 13f).toInt().coerceIn(0, 11) fontSizeSeekBar.progress = initialProgress @@ -4663,29 +4822,32 @@ open class PocketChatActivity : AppCompatActivity() { previewSettingsFromDialog(originalSettings, draftSettings) } - var ignoreInitialAccentSelection = true - accentColorSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (ignoreInitialAccentSelection && position == initialAccentIndex) { - ignoreInitialAccentSelection = false - return - } - ignoreInitialAccentSelection = false - val selectedAccent = accentOptions.getOrNull(position) ?: return - if (draftSettings.accent == selectedAccent) { - return - } - draftSettings = draftSettings.copy(accent = selectedAccent) - previewSettingsFromDialog(originalSettings, draftSettings) + fun renderAccentSwatches() { + accentSwatchRow.removeAllViews() + val ringColor = resolveThemeColor(R.attr.colorAssistantText) + accentOptions.forEachIndexed { index, accent -> + val selected = accent == draftSettings.accent + accentSwatchRow.addView(View(dialogContext).apply { + layoutParams = LinearLayout.LayoutParams(dp(36), dp(36)).apply { + if (index > 0) marginStart = dp(10) + } + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(ContextCompat.getColor(dialogContext, accent.swatchColorRes)) + if (selected) setStroke(dp(3), ringColor) + } + contentDescription = dialogContext.getString(accent.labelResId) + isSelected = selected + setOnClickListener { + if (draftSettings.accent == accent) return@setOnClickListener + draftSettings = draftSettings.copy(accent = accent) + renderAccentSwatches() + previewSettingsFromDialog(originalSettings, draftSettings) + } + }) } - - override fun onNothingSelected(parent: AdapterView<*>?) = Unit } + renderAccentSwatches() fontSizeSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { @@ -4715,11 +4877,7 @@ open class PocketChatActivity : AppCompatActivity() { } saveButton.setOnClickListener { - val selectedAccent = accentOptions - .getOrNull(accentColorSpinner.selectedItemPosition) - ?: draftSettings.accent val updatedSettings = draftSettings.copy( - accent = selectedAccent, chatFontSizeSp = 13f + fontSizeSeekBar.progress ) val visualThemeChanged = updatedSettings.accent != currentSettings.accent || @@ -4832,7 +4990,7 @@ open class PocketChatActivity : AppCompatActivity() { val modelNameView: TextView = dialogView.findViewById(R.id.modelSettingsModelName) val contextLengthInput: EditText = dialogView.findViewById(R.id.modelContextLengthInput) val contextLengthHelp: TextView = dialogView.findViewById(R.id.modelContextLengthHelp) - val presetSpinner: AppCompatSpinner = dialogView.findViewById(R.id.modelInstructionPresetSpinner) + val presetChipGroup: ChipGroup = dialogView.findViewById(R.id.modelInstructionPresetChips) val instructionInput: EditText = dialogView.findViewById(R.id.modelInstructionInput) val cancelButton: Button = dialogView.findViewById(R.id.modelSettingsCancelButton) val saveButton: Button = dialogView.findViewById(R.id.modelSettingsSaveButton) @@ -4872,15 +5030,14 @@ open class PocketChatActivity : AppCompatActivity() { } } val presets = InstructionPreset.entries.toList() - val presetLabels = presets.map { it.label } - val presetAdapter = ArrayAdapter( - this, - R.layout.item_instruction_preset_spinner, - presetLabels - ).apply { - setDropDownViewResource(R.layout.item_instruction_preset_dropdown) + val presetChipInflater = LayoutInflater.from(dialogView.context) + val presetChipIds = presets.map { preset -> + val chip = presetChipInflater.inflate(R.layout.item_preset_chip, presetChipGroup, false) as Chip + chip.id = View.generateViewId() + chip.text = preset.label + presetChipGroup.addView(chip) + chip.id } - presetSpinner.adapter = presetAdapter var selectedPreset = modelInstructionStore.loadPreset(descriptor) var applyingPresetText = false @@ -4893,43 +5050,32 @@ open class PocketChatActivity : AppCompatActivity() { } else { InstructionPreset.CUSTOM.instruction } - var ignoreInitialPresetSelection = true - presetSpinner.setSelection(selectedPresetIndex, false) + presetChipGroup.check(presetChipIds[selectedPresetIndex]) applyingPresetText = true instructionInput.setText(currentInstruction) instructionInput.setSelection(currentInstruction.length) applyingPresetText = false - presetSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - val preset = presets.getOrNull(position) ?: return - if (ignoreInitialPresetSelection && position == selectedPresetIndex) { - ignoreInitialPresetSelection = false - return - } - ignoreInitialPresetSelection = false - selectedPreset = preset - if (switchingToCustomFromEdit) { - return - } - - applyingPresetText = true - val presetInstruction = if (preset == InstructionPreset.CUSTOM) { - customInstructionText - } else { - preset.instruction - } - instructionInput.setText(presetInstruction) - instructionInput.setSelection(instructionInput.text.length) - applyingPresetText = false + presetChipGroup.setOnCheckedStateChangeListener { _, checkedIds -> + val position = presetChipIds.indexOf(checkedIds.firstOrNull() ?: return@setOnCheckedStateChangeListener) + val preset = presets.getOrNull(position) ?: return@setOnCheckedStateChangeListener + if (preset == selectedPreset) { + return@setOnCheckedStateChangeListener + } + selectedPreset = preset + if (switchingToCustomFromEdit) { + return@setOnCheckedStateChangeListener } - override fun onNothingSelected(parent: AdapterView<*>?) = Unit + applyingPresetText = true + val presetInstruction = if (preset == InstructionPreset.CUSTOM) { + customInstructionText + } else { + preset.instruction + } + instructionInput.setText(presetInstruction) + instructionInput.setSelection(instructionInput.text.length) + applyingPresetText = false } instructionInput.addTextChangedListener(object : TextWatcher { @@ -4965,9 +5111,10 @@ open class PocketChatActivity : AppCompatActivity() { customInstructionText = editedInstruction selectedPreset = InstructionPreset.CUSTOM val customPresetIndex = presets.indexOf(InstructionPreset.CUSTOM) - if (presetSpinner.selectedItemPosition != customPresetIndex) { + val customChipId = presetChipIds[customPresetIndex] + if (presetChipGroup.checkedChipId != customChipId) { switchingToCustomFromEdit = true - presetSpinner.setSelection(customPresetIndex) + presetChipGroup.check(customChipId) switchingToCustomFromEdit = false } } @@ -5209,6 +5356,9 @@ open class PocketChatActivity : AppCompatActivity() { ) { fontSizeValue.text = "${fontSizeSp.toInt()} sp" fontSizePreview.textSize = fontSizeSp + (fontSizePreview.parent as? View) + ?.findViewById(R.id.fontSizePreviewReply) + ?.textSize = fontSizeSp } private fun updateProgressBar( @@ -5248,12 +5398,14 @@ open class PocketChatActivity : AppCompatActivity() { } private fun applyStatusBackground(message: String) { - val backgroundRes = when (message) { - MODEL_LOADING_STATUS_MESSAGE -> R.drawable.bg_status_loading - MODEL_READY_STATUS_MESSAGE -> R.drawable.bg_status_ready - else -> R.drawable.bg_status_chip + // Plain italic text rather than a chip, so the status never reads as a button. + val textColor = when (message) { + MODEL_LOADING_STATUS_MESSAGE -> ContextCompat.getColor(this, R.color.status_loading_background) + MODEL_READY_STATUS_MESSAGE -> ContextCompat.getColor(this, R.color.status_ready_background) + else -> resolveThemeColor(R.attr.colorStatusText) } - statusView.setBackgroundResource(backgroundRes) + statusView.background = null + statusView.setTextColor(textColor) } private fun updateMicRecordingState(active: Boolean) { diff --git a/pocket_llm_src/app/src/main/res/color/chip_background.xml b/pocket_llm_src/app/src/main/res/color/chip_background.xml new file mode 100644 index 0000000..a9803ae --- /dev/null +++ b/pocket_llm_src/app/src/main/res/color/chip_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/color/chip_stroke.xml b/pocket_llm_src/app/src/main/res/color/chip_stroke.xml new file mode 100644 index 0000000..5ca980f --- /dev/null +++ b/pocket_llm_src/app/src/main/res/color/chip_stroke.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/color/segment_text.xml b/pocket_llm_src/app/src/main/res/color/segment_text.xml new file mode 100644 index 0000000..91f6176 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/color/segment_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_status_ready.xml b/pocket_llm_src/app/src/main/res/drawable/bg_accent_pill.xml similarity index 59% rename from pocket_llm_src/app/src/main/res/drawable/bg_status_ready.xml rename to pocket_llm_src/app/src/main/res/drawable/bg_accent_pill.xml index ae5b578..c29bd43 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_status_ready.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_accent_pill.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_bubble_bot.xml b/pocket_llm_src/app/src/main/res/drawable/bg_bubble_bot.xml index 29c7065..9b8bbe1 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_bubble_bot.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_bubble_bot.xml @@ -1,18 +1,15 @@ - + + android:topLeftRadius="20dp" + android:topRightRadius="20dp" + android:bottomLeftRadius="6dp" + android:bottomRightRadius="20dp" /> diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_bubble_user.xml b/pocket_llm_src/app/src/main/res/drawable/bg_bubble_user.xml index e47a35c..99de84b 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_bubble_user.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_bubble_user.xml @@ -3,13 +3,13 @@ android:shape="rectangle"> + android:topLeftRadius="20dp" + android:topRightRadius="20dp" + android:bottomLeftRadius="20dp" + android:bottomRightRadius="6dp" /> diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_status_loading.xml b/pocket_llm_src/app/src/main/res/drawable/bg_card.xml similarity index 59% rename from pocket_llm_src/app/src/main/res/drawable/bg_status_loading.xml rename to pocket_llm_src/app/src/main/res/drawable/bg_card.xml index 5ff48a8..e506db3 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_status_loading.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_card.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_composer_pill.xml b/pocket_llm_src/app/src/main/res/drawable/bg_composer_pill.xml new file mode 100644 index 0000000..ddf14d5 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_composer_pill.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_context_bar.xml b/pocket_llm_src/app/src/main/res/drawable/bg_context_bar.xml new file mode 100644 index 0000000..7152e51 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_context_bar.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_dialog_panel.xml b/pocket_llm_src/app/src/main/res/drawable/bg_dialog_panel.xml index 4852f5a..1cdb162 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_dialog_panel.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_dialog_panel.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_drawer_row.xml b/pocket_llm_src/app/src/main/res/drawable/bg_drawer_row.xml new file mode 100644 index 0000000..6bbee96 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_drawer_row.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_image_input_chip.xml b/pocket_llm_src/app/src/main/res/drawable/bg_image_input_chip.xml index 9403951..321bf28 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_image_input_chip.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_image_input_chip.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_input_panel.xml b/pocket_llm_src/app/src/main/res/drawable/bg_input_panel.xml index 4852f5a..48563dc 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_input_panel.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_input_panel.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_logo_tile.xml b/pocket_llm_src/app/src/main/res/drawable/bg_logo_tile.xml new file mode 100644 index 0000000..6147f52 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_logo_tile.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_model_chip.xml b/pocket_llm_src/app/src/main/res/drawable/bg_model_chip.xml new file mode 100644 index 0000000..c054a9a --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_model_chip.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_model_picker_panel.xml b/pocket_llm_src/app/src/main/res/drawable/bg_model_picker_panel.xml index 76621d6..791fb6e 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_model_picker_panel.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_model_picker_panel.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_popup_menu.xml b/pocket_llm_src/app/src/main/res/drawable/bg_popup_menu.xml new file mode 100644 index 0000000..8846485 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_popup_menu.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_preview_box.xml b/pocket_llm_src/app/src/main/res/drawable/bg_preview_box.xml new file mode 100644 index 0000000..76621d6 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_preview_box.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_round_raised.xml b/pocket_llm_src/app/src/main/res/drawable/bg_round_raised.xml new file mode 100644 index 0000000..8dc6386 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_round_raised.xml @@ -0,0 +1,5 @@ + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_saved_chat_item.xml b/pocket_llm_src/app/src/main/res/drawable/bg_saved_chat_item.xml index 6e95d25..59dec50 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_saved_chat_item.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_saved_chat_item.xml @@ -1,6 +1,6 @@ - - + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_segment_option.xml b/pocket_llm_src/app/src/main/res/drawable/bg_segment_option.xml new file mode 100644 index 0000000..ac328c6 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_segment_option.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_segment_track.xml b/pocket_llm_src/app/src/main/res/drawable/bg_segment_track.xml new file mode 100644 index 0000000..9b96fa4 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_segment_track.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_status_chip.xml b/pocket_llm_src/app/src/main/res/drawable/bg_status_chip.xml index ce3ef0c..4852f5a 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_status_chip.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_status_chip.xml @@ -2,5 +2,5 @@ - + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_status_dot.xml b/pocket_llm_src/app/src/main/res/drawable/bg_status_dot.xml new file mode 100644 index 0000000..d043d26 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/bg_status_dot.xml @@ -0,0 +1,5 @@ + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/bg_toolbar_logo_white.xml b/pocket_llm_src/app/src/main/res/drawable/bg_toolbar_logo_white.xml index 9cf9a8d..b20251c 100644 --- a/pocket_llm_src/app/src/main/res/drawable/bg_toolbar_logo_white.xml +++ b/pocket_llm_src/app/src/main/res/drawable/bg_toolbar_logo_white.xml @@ -2,5 +2,5 @@ - + diff --git a/pocket_llm_src/app/src/main/res/drawable/btn_send.xml b/pocket_llm_src/app/src/main/res/drawable/btn_send.xml index 5c157f7..12c80dc 100644 --- a/pocket_llm_src/app/src/main/res/drawable/btn_send.xml +++ b/pocket_llm_src/app/src/main/res/drawable/btn_send.xml @@ -2,8 +2,5 @@ - - + diff --git a/pocket_llm_src/app/src/main/res/drawable/btn_stop.xml b/pocket_llm_src/app/src/main/res/drawable/btn_stop.xml index e69737c..3bb12e9 100644 --- a/pocket_llm_src/app/src/main/res/drawable/btn_stop.xml +++ b/pocket_llm_src/app/src/main/res/drawable/btn_stop.xml @@ -2,8 +2,5 @@ - - + diff --git a/pocket_llm_src/app/src/main/res/drawable/logo_about.xml b/pocket_llm_src/app/src/main/res/drawable/logo_about.xml new file mode 100644 index 0000000..d007730 --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/logo_about.xml @@ -0,0 +1,9 @@ + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/logo_drawer.xml b/pocket_llm_src/app/src/main/res/drawable/logo_drawer.xml new file mode 100644 index 0000000..b24eebb --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/logo_drawer.xml @@ -0,0 +1,9 @@ + + + + diff --git a/pocket_llm_src/app/src/main/res/drawable/rounded_edittext.xml b/pocket_llm_src/app/src/main/res/drawable/rounded_edittext.xml index 9cdbeda..04cf9e6 100644 --- a/pocket_llm_src/app/src/main/res/drawable/rounded_edittext.xml +++ b/pocket_llm_src/app/src/main/res/drawable/rounded_edittext.xml @@ -1,9 +1,9 @@ - + - + android:width="1dp" + android:color="?attr/colorPocketOutline" /> + diff --git a/pocket_llm_src/app/src/main/res/drawable/toolbar_logo_small.xml b/pocket_llm_src/app/src/main/res/drawable/toolbar_logo_small.xml new file mode 100644 index 0000000..863a44b --- /dev/null +++ b/pocket_llm_src/app/src/main/res/drawable/toolbar_logo_small.xml @@ -0,0 +1,9 @@ + + + + diff --git a/pocket_llm_src/app/src/main/res/layout/activity_main.xml b/pocket_llm_src/app/src/main/res/layout/activity_main.xml index 23190f1..53f78b0 100644 --- a/pocket_llm_src/app/src/main/res/layout/activity_main.xml +++ b/pocket_llm_src/app/src/main/res/layout/activity_main.xml @@ -19,7 +19,7 @@ android:id="@+id/topToolbar" android:layout_width="0dp" android:layout_height="wrap_content" - android:background="@drawable/bg_chat_surface" + android:background="?attr/colorAppBackground" android:minHeight="56dp" app:contentInsetEnd="0dp" app:contentInsetLeft="0dp" @@ -36,32 +36,43 @@ android:layout_height="match_parent" android:layout_gravity="fill" android:minHeight="56dp" - android:paddingHorizontal="8dp" - android:paddingVertical="3dp"> + android:paddingHorizontal="6dp" + android:paddingVertical="4dp"> + + @@ -69,44 +80,75 @@ android:id="@+id/toolbarTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:includeFontPadding="false" - android:text="@string/toolbar_app_title" - android:textAlignment="center" - android:textColor="?attr/colorAssistantText" - android:textSize="18sp" - android:textStyle="bold" /> + android:text="@string/toolbar_app_title" /> + + + + + + + + android:textSize="13sp" /> - + app:layout_constraintTop_toTopOf="parent" + app:trackColor="?attr/colorPocketOutline" + app:trackCornerRadius="2dp" + app:trackThickness="3dp" /> @@ -114,8 +156,7 @@ android:id="@+id/chatFrame" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginTop="10dp" - android:background="@drawable/bg_chat_surface" + android:background="?attr/colorAppBackground" app:layout_constraintBottom_toTopOf="@id/chatControlsRow" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -127,19 +168,21 @@ android:layout_height="match_parent" android:clipToPadding="false" android:overScrollMode="always" - android:padding="10dp" /> + android:paddingHorizontal="12dp" + android:paddingVertical="8dp" /> @@ -154,7 +197,7 @@ android:maxWidth="260dp" android:paddingHorizontal="18dp" android:paddingVertical="8dp" - android:textColor="?attr/colorStatusText" + android:textColor="?attr/colorAssistantText" android:textSize="14sp" android:textStyle="bold" android:visibility="gone" @@ -165,102 +208,27 @@ android:id="@+id/chatControlsRow" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="1dp" android:gravity="center_vertical" android:orientation="horizontal" - android:paddingHorizontal="2dp" - android:paddingVertical="1dp" + android:paddingHorizontal="12dp" + app:layout_constraintBottom_toTopOf="@id/inputRow" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/chatFrame"> - - - - - - - - - - - - - - @@ -293,45 +261,22 @@ android:id="@+id/inputRow" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="2dp" - android:layout_marginBottom="4dp" android:background="@drawable/bg_input_panel" - android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="8dp" - android:paddingVertical="7dp" + android:paddingHorizontal="12dp" + android:paddingTop="8dp" + android:paddingBottom="12dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/chatControlsRow"> + app:layout_constraintStart_toStartOf="parent"> - - + android:padding="6dp"> - + + - - + android:background="@android:color/transparent" + android:gravity="center_vertical|start" + android:hint="@string/enter_your_prompt" + android:imeOptions="actionSend" + android:inputType="textMultiLine|textCapSentences" + android:maxLines="6" + android:minHeight="40dp" + android:minLines="1" + android:paddingHorizontal="6dp" + android:paddingVertical="8dp" + android:textColor="?attr/colorInputText" + android:textColorHint="?attr/colorInputHint" + android:textSize="16sp" /> - + - + - - - - + + android:layout_marginStart="4dp"> + android:text="@string/send" /> + android:text="@string/stop" + android:visibility="gone" /> @@ -576,23 +490,74 @@ android:layout_width="304dp" android:layout_height="match_parent" android:layout_gravity="start" - android:background="?attr/colorFrameBackground" + android:background="?attr/colorAppBackground" android:elevation="16dp" android:fitsSystemWindows="true" android:orientation="vertical" - android:paddingTop="20dp" + android:paddingTop="16dp" android:paddingBottom="12dp"> - + + + + + + + + android:orientation="horizontal"> + + + + + - - + android:textColor="@color/pocket_faint" + android:textSize="13sp" /> + android:background="?attr/colorPocketOutline" /> + android:layout_marginHorizontal="10dp" + android:paddingHorizontal="12dp"> + android:textSize="15sp" /> + android:layout_marginHorizontal="10dp" + android:paddingHorizontal="12dp"> + android:textSize="15sp" /> + android:layout_marginHorizontal="10dp" + android:paddingHorizontal="12dp"> + android:textSize="15sp" /> + android:textColor="@color/pocket_faint" + android:textSize="13sp" /> + android:layout_marginHorizontal="10dp" + android:paddingHorizontal="12dp"> + android:textSize="15sp" /> + android:layout_marginHorizontal="10dp" + android:paddingHorizontal="12dp"> + android:textSize="15sp" /> + android:layout_marginHorizontal="10dp" + android:paddingHorizontal="12dp"> + android:textSize="15sp" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_about.xml b/pocket_llm_src/app/src/main/res/layout/dialog_about.xml index 55a339f..fc637b7 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_about.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_about.xml @@ -3,13 +3,26 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_dialog_panel" + android:gravity="center_horizontal" android:orientation="vertical" android:padding="20dp"> + + + android:textSize="14sp" /> + android:textColorLink="?attr/colorSendFill" + android:textSize="14sp" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_audio_recording.xml b/pocket_llm_src/app/src/main/res/layout/dialog_audio_recording.xml index 59e4867..61dd294 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_audio_recording.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_audio_recording.xml @@ -3,46 +3,63 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:paddingHorizontal="24dp" - android:paddingVertical="16dp"> + android:paddingHorizontal="20dp" + android:paddingTop="12dp" + android:paddingBottom="16dp"> - + android:layout_gravity="center_horizontal" + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + android:textColor="?attr/colorAssistantText" + android:textSize="48sp" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_camera_crop.xml b/pocket_llm_src/app/src/main/res/layout/dialog_camera_crop.xml index 16dffa7..86ddbfc 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_camera_crop.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_camera_crop.xml @@ -15,8 +15,8 @@ android:layout_height="wrap_content" android:gravity="center" android:text="@string/camera_crop_title" - android:textColor="?attr/colorAssistantText" - android:textSize="20sp" + android:textColor="?attr/colorSendFill" + android:textSize="22sp" android:textStyle="bold" /> + + + + + + + + + + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_custom_model_warning.xml b/pocket_llm_src/app/src/main/res/layout/dialog_custom_model_warning.xml index 4336dc7..421ccfc 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_custom_model_warning.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_custom_model_warning.xml @@ -12,8 +12,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/custom_model_warning_title" - android:textColor="?attr/colorStopStroke" - android:textSize="20sp" + android:textColor="?attr/colorStopFill" + android:textSize="22sp" android:textStyle="bold" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_dictation_download.xml b/pocket_llm_src/app/src/main/res/layout/dialog_dictation_download.xml index 236fe0b..871a12f 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_dictation_download.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_dictation_download.xml @@ -10,29 +10,31 @@ android:id="@+id/dictationDownloadMessage" android:layout_width="match_parent" android:layout_height="wrap_content" - android:textColor="?attr/colorAssistantText" + android:lineSpacingExtra="2dp" + android:textColor="?attr/colorStatusText" android:textSize="15sp" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_image_library.xml b/pocket_llm_src/app/src/main/res/layout/dialog_image_library.xml index 317f943..a9928f6 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_image_library.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_image_library.xml @@ -12,8 +12,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/images_section_title" - android:textColor="?attr/colorAssistantText" - android:textSize="20sp" + android:textColor="?attr/colorSendFill" + android:textSize="22sp" android:textStyle="bold" /> + android:text="@string/delete_selected_images" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_lan_server.xml b/pocket_llm_src/app/src/main/res/layout/dialog_lan_server.xml index 4174d19..12e4e14 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_lan_server.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_lan_server.xml @@ -6,123 +6,222 @@ android:layout_height="wrap_content" android:background="@drawable/bg_dialog_panel" android:orientation="vertical" - android:padding="20dp"> + android:paddingHorizontal="16dp" + android:paddingTop="20dp" + android:paddingBottom="16dp"> + + + android:orientation="horizontal" + android:padding="18dp"> - + + + android:orientation="vertical"> + + + + + android:paddingHorizontal="4dp" + android:paddingTop="16dp" + android:paddingBottom="8dp" + android:text="@string/lan_server_section_open" + android:textColor="@color/pocket_faint" + android:textSize="13sp" /> - - - + android:background="@drawable/bg_card" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingVertical="12dp" + android:paddingStart="16dp" + android:paddingEnd="8dp"> - + - + - + + + + + + + android:layout_marginTop="10dp" + android:background="@drawable/bg_card" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingVertical="12dp" + android:paddingStart="16dp" + android:paddingEnd="8dp"> - + + + + + + + + + + android:orientation="horizontal" + android:paddingVertical="8dp" + android:paddingStart="16dp" + android:paddingEnd="8dp"> - + android:orientation="vertical"> + + + + + @@ -132,60 +231,44 @@ style="@style/DialogSecondaryButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="8dp" android:text="@string/lan_server_save_password" android:visibility="gone" /> - - - - + android:layout_marginTop="10dp" + android:paddingHorizontal="4dp" + android:text="@string/lan_server_battery_note" + android:textColor="?attr/colorStatusText" + android:textSize="13sp" /> + android:text="@string/lan_server_stop" /> diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_model_selection.xml b/pocket_llm_src/app/src/main/res/layout/dialog_model_selection.xml index 7d38fc7..01b65b6 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_model_selection.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_model_selection.xml @@ -2,20 +2,20 @@ + android:paddingHorizontal="16dp" + android:paddingTop="20dp" + android:paddingBottom="12dp"> @@ -23,27 +23,20 @@ android:id="@+id/modelPickerIntro" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="10dp" - android:gravity="center" + android:layout_marginTop="8dp" android:lineSpacingExtra="2dp" + android:paddingHorizontal="4dp" android:textColor="?attr/colorStatusText" android:textSize="13sp" /> - - + android:layout_height="0dp" + android:layout_marginTop="14dp" + android:layout_weight="1" + android:fillViewport="false" + android:overScrollMode="ifContentScrolls" + android:scrollbars="none"> + + diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_model_settings.xml b/pocket_llm_src/app/src/main/res/layout/dialog_model_settings.xml index a87b0ae..6539295 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_model_settings.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_model_settings.xml @@ -1,138 +1,197 @@ + android:paddingHorizontal="16dp" + android:paddingTop="20dp" + android:paddingBottom="16dp"> - - - + android:layout_weight="1" + android:fillViewport="false" + android:overScrollMode="ifContentScrolls" + android:scrollbars="none"> - + - - - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/layout/dialog_settings.xml b/pocket_llm_src/app/src/main/res/layout/dialog_settings.xml index a474b34..a8ad21b 100644 --- a/pocket_llm_src/app/src/main/res/layout/dialog_settings.xml +++ b/pocket_llm_src/app/src/main/res/layout/dialog_settings.xml @@ -4,134 +4,214 @@ android:layout_height="wrap_content" android:background="@drawable/bg_dialog_panel" android:orientation="vertical" - android:padding="20dp"> + android:paddingHorizontal="16dp" + android:paddingTop="20dp" + android:paddingBottom="16dp"> - - - + android:layout_marginTop="16dp" + android:background="@drawable/bg_card" + android:orientation="vertical" + android:padding="16dp"> - + android:text="@string/appearance_mode_label" + android:textColor="?attr/colorStatusText" + android:textSize="13sp" /> - - + android:layout_marginTop="10dp" + android:background="@drawable/bg_segment_track" + android:orientation="horizontal" + android:padding="4dp"> - + - + + - + - - - + - + + + + + android:layout_marginTop="10dp" + android:background="@drawable/bg_card" + android:orientation="vertical" + android:padding="16dp"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/layout/item_drawer_chat.xml b/pocket_llm_src/app/src/main/res/layout/item_drawer_chat.xml index 879c3cf..302d680 100644 --- a/pocket_llm_src/app/src/main/res/layout/item_drawer_chat.xml +++ b/pocket_llm_src/app/src/main/res/layout/item_drawer_chat.xml @@ -23,8 +23,7 @@ android:ellipsize="end" android:maxLines="1" android:textColor="?attr/colorAssistantText" - android:textSize="15sp" - android:textStyle="bold" /> + android:textSize="15sp" /> + app:tint="@color/pocket_faint" /> diff --git a/pocket_llm_src/app/src/main/res/layout/item_instruction_preset_dropdown.xml b/pocket_llm_src/app/src/main/res/layout/item_instruction_preset_dropdown.xml deleted file mode 100644 index a4970a2..0000000 --- a/pocket_llm_src/app/src/main/res/layout/item_instruction_preset_dropdown.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/pocket_llm_src/app/src/main/res/layout/item_instruction_preset_spinner.xml b/pocket_llm_src/app/src/main/res/layout/item_instruction_preset_spinner.xml deleted file mode 100644 index 555a78b..0000000 --- a/pocket_llm_src/app/src/main/res/layout/item_instruction_preset_spinner.xml +++ /dev/null @@ -1,10 +0,0 @@ - - diff --git a/pocket_llm_src/app/src/main/res/layout/item_message.xml b/pocket_llm_src/app/src/main/res/layout/item_message.xml index d14fcc0..b6c6fd8 100644 --- a/pocket_llm_src/app/src/main/res/layout/item_message.xml +++ b/pocket_llm_src/app/src/main/res/layout/item_message.xml @@ -21,12 +21,11 @@ android:id="@+id/preResponseStatusView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/bg_status_chip" - android:paddingHorizontal="12dp" - android:paddingVertical="7dp" + android:paddingHorizontal="6dp" + android:paddingVertical="4dp" android:textColor="?attr/colorStatusText" android:textSize="14sp" - android:textStyle="bold" /> + android:textStyle="normal" /> + android:textStyle="normal" /> + + diff --git a/pocket_llm_src/app/src/main/res/layout/item_model_option.xml b/pocket_llm_src/app/src/main/res/layout/item_model_option.xml index e010a1d..a1cc878 100644 --- a/pocket_llm_src/app/src/main/res/layout/item_model_option.xml +++ b/pocket_llm_src/app/src/main/res/layout/item_model_option.xml @@ -4,10 +4,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" - android:background="@drawable/bg_saved_chat_item" + android:background="@drawable/bg_card" android:orientation="vertical" - android:paddingHorizontal="12dp" - android:paddingVertical="10dp"> + android:paddingVertical="12dp" + android:paddingStart="16dp" + android:paddingEnd="10dp"> + android:textSize="12.5sp" /> + diff --git a/pocket_llm_src/app/src/main/res/layout/popup_model_picker.xml b/pocket_llm_src/app/src/main/res/layout/popup_model_picker.xml index ae3bbd8..1e4945c 100644 --- a/pocket_llm_src/app/src/main/res/layout/popup_model_picker.xml +++ b/pocket_llm_src/app/src/main/res/layout/popup_model_picker.xml @@ -4,7 +4,7 @@ android:layout_height="wrap_content" android:background="@drawable/bg_model_picker_panel" android:orientation="vertical" - android:paddingHorizontal="10dp" + android:paddingHorizontal="8dp" android:paddingTop="8dp" android:paddingBottom="6dp"> @@ -27,8 +27,7 @@ android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="4dp" - android:alpha="0.16" - android:background="?attr/colorStatusText" /> + android:background="?attr/colorPocketOutline" /> + + diff --git a/pocket_llm_src/app/src/main/res/values/colors.xml b/pocket_llm_src/app/src/main/res/values/colors.xml index 3758fa7..1795232 100644 --- a/pocket_llm_src/app/src/main/res/values/colors.xml +++ b/pocket_llm_src/app/src/main/res/values/colors.xml @@ -6,22 +6,22 @@ #FF03DAC5 #FF018786 #FF000000 - #111111 - #1A1A1A - #F7ECDD - #2A6CAF - #0B213D - #F6FAFF - #111111 - #F6FAFF - #8A8A8A - #5EB9FF - #BCE1FF - #F08A7A - #FFC0B5 - #D8E9FF - #F08A8A - #7CCF8A + #16171B + #16171B + #302C44 + #212228 + #EDEDF1 + #EDEDF1 + #212228 + #EDEDF1 + #6E707B + #A98BFF + #C7B4FF + #F07A6E + #F7AAA2 + #9E9FA9 + #F2B35B + #5FD39A #F2F5F8 #FAFBFD #F7F9FC @@ -101,6 +101,28 @@ #E66E58 #FF7D79 #FFAF61 + #212228 + #2A2B32 + #30313A + #6E707B + #5FD39A + #140E24 + #2A0E0A + #ECE5FF + #9677F5 + #FFFFFF + #5EB9FF + #BCE1FF + #233444 + #262D44 + #223A37 + #2E2644 + #40341D + #402B2A + #33F07A6E #FF4D4D + #F2B35B + #5FD39A + #7FB2FF #FFFFFFFF diff --git a/pocket_llm_src/app/src/main/res/values/strings.xml b/pocket_llm_src/app/src/main/res/values/strings.xml index e1064ae..9a55ea7 100644 --- a/pocket_llm_src/app/src/main/res/values/strings.xml +++ b/pocket_llm_src/app/src/main/res/values/strings.xml @@ -7,10 +7,11 @@ Change Models Type your message Send + %1$.1f tok/s Stop Voice input Record dictation - Temporary recording · transcribe when ready + Recording · stays on this phone Transcribe Discard Transcribing recording… @@ -78,9 +79,12 @@ Chat deleted. Copy response Response copied. - Thinking Mode + Thinking mode + Thinking + Think before answering + Thinking New Chat - Previous Chats + Chats Pick a conversation to reopen and continue from the same history. Tap to reopen this conversation. No saved chats yet. @@ -89,15 +93,19 @@ LAN Server Stopped Starting model server… - Running on the local network. Use the Web UI URL for browser chat or the API base URL for scripts and Open WebUI. + Running on Wi-Fi + Use the browser address for chat, or the API address for scripts and Open WebUI. + Share the loaded model with devices on the same Wi-Fi. + Open from another device + Keeps running with the screen off and uses more battery. Could not start the LAN server - Browser chat URL (/ui) - API URL (/v1) + Browser chat + OpenAI-compatible API API base URL Copy API URL API URL copied. Web UI URL - Web UI password + Password Set a password before starting Change password Show password @@ -105,7 +113,7 @@ Save password Save the new password first. Could not save the password. - Start + Start server Copy browser URL Browser URL copied. Stop the LAN server and close @@ -122,20 +130,20 @@ Loading the selected model… Screen-off server active at %1$s · Uses battery Stop server - Chat Settings + Settings About About Version %1$s https://github.com/dineshsoudagar/local-llms-on-android Github Link : https://github.com/dineshsoudagar/local-llms-on-android - Model Settings + Model settings Choose a model to configure Download a model before changing its context length or instructions. Model settings saved. - MODELS - Manage Models + Models + Models Download or import chat models, then choose one to use. - use your own model (beta) + + Import your own model (beta) Use your own LiteRT model Choose a local .litertlm file. The app detects its inputs and attempts native image and audio support when available. You can also use OCR for images. Video input is not enabled. The app is not optimized or tested for your model. It may fail to load or crash. Use at your own risk. @@ -147,8 +155,9 @@ No downloaded text models Manage models → %1$s · %2$s - Model Settings - Context length (tokens) + Model settings + Context length + tokens For example: 512 Higher values retain more chat history but use more memory. Allowed range: %1$d–%2$d tokens. The model may need to reload after this changes. Enter a context length from %1$d to %2$d tokens. @@ -193,19 +202,21 @@ Appearance Dark Light - Accent Color + Accent color + Pocket purple Blue Indigo Green Violet Amber Coral - Chat Font Size - Preview text for the chat bubbles + Text size + How big is this text? + This is how replies will look. Save Enable Thinking Choose a model - Manage Models + Models Choose a chat model before chatting offline.\nThe image input mode converts attached images into context before sending. Pick chat and image input modes anytime.\nUse OCR for images or native image input when supported by the selected model and device backend. %1$s - %2$s download @@ -244,9 +255,9 @@ Not downloaded Downloading... Unavailable - ✓ + In use Use - ↓ + Download ... Unavailable Download @@ -311,6 +322,14 @@ Audio recording could not start. Downloading transcription model: %1$d%% Context used by this chat - This chat uses about %1$,d of %2$,d tokens (%3$d%%). Above 75%%, older messages are summarized to make room. - This chat uses about %1$,d of %2$,d tokens (%3$d%%). When it is full, the oldest messages are left out. + of %1$,d tokens + %1$d%% + Instruction + Summary of earlier messages + Document passages + Conversation + Free space + %1$,d + Above 75%%, older messages are summarized to make room. + When it is full, the oldest messages are left out. diff --git a/pocket_llm_src/app/src/main/res/values/styles.xml b/pocket_llm_src/app/src/main/res/values/styles.xml index e1e97a6..571c31a 100644 --- a/pocket_llm_src/app/src/main/res/values/styles.xml +++ b/pocket_llm_src/app/src/main/res/values/styles.xml @@ -9,52 +9,64 @@ 8dp 92dp 56dp - 18dp + 20dp 0dp #FFFFFF @@ -64,17 +76,17 @@ 0dp false bold - 12sp + 13.5sp 6dp 6dp 0dp 0dp - 10dp + 14dp 0dp ?attr/colorSendFill - ?attr/colorSendStroke + ?attr/colorSendFill 1dp - @color/white + @color/on_accent + + + + + + + + + + + + + + + + + diff --git a/pocket_llm_src/app/src/main/res/values/themes.xml b/pocket_llm_src/app/src/main/res/values/themes.xml index 6616cc9..f3e5c95 100644 --- a/pocket_llm_src/app/src/main/res/values/themes.xml +++ b/pocket_llm_src/app/src/main/res/values/themes.xml @@ -4,17 +4,19 @@ @color/app_background @color/white @color/assistant_text - @color/frame_layout_background + @color/pocket_surface @color/assistant_text ?attr/colorAppBackground @color/app_background - @color/frame_layout_background + @color/pocket_surface + @color/pocket_raised + @color/pocket_line @color/user_bubble @color/bot_bubble @color/user_text @color/assistant_text - @color/app_background - @color/assistant_text + @color/input_background + @color/input_text @color/input_hint @color/status_text @color/send_button_fill @@ -25,6 +27,18 @@ @color/bot_bubble @color/send_button_fill @color/send_button_fill + ?attr/colorSendFill + ?attr/colorSendFill + ?attr/colorAppBackground + @style/ThemeOverlay.Pocket.AlertDialog + @style/Pocket.PopupMenu + @style/Pocket.PopupMenu + @color/assistant_text + @color/status_text + @color/pocket_faint + @color/input_hint + @color/assistant_text + ?attr/colorSendFill false ?attr/colorAppBackground @@ -34,160 +48,128 @@ @color/light_app_background @color/light_assistant_text @color/light_assistant_text - @color/light_frame_background + @color/light_surface @color/light_assistant_text ?attr/colorAppBackground @color/light_app_background - @color/light_frame_background - @color/light_assistant_bubble - @color/light_user_bubble + @color/light_surface + @color/light_frame_background + @color/light_control_stroke + @color/light_pocket_user_bubble + @color/light_surface @color/light_user_text @color/light_assistant_text - @color/light_input_background + @color/light_surface @color/light_input_text @color/light_input_hint @color/light_status_text + @color/light_pocket_send_fill @color/light_control_stroke @color/light_stop_stroke - @color/light_user_bubble - @color/light_user_bubble - @color/send_button_fill - @color/send_button_fill + @color/light_surface + @color/light_surface + @color/light_pocket_send_fill + @color/light_pocket_send_fill ?attr/colorSendFill ?attr/colorSendStroke ?attr/colorSendFill ?attr/colorSendStroke + ?attr/colorAppBackground + @color/light_assistant_text + @color/light_status_text + @color/light_input_hint + @color/light_input_hint + @color/light_assistant_text true ?attr/colorAppBackground + + + +