diff --git a/examples/llama.android/app/build.gradle.kts b/examples/llama.android/app/build.gradle.kts index 0317c41d89..f2e630dd93 100644 --- a/examples/llama.android/app/build.gradle.kts +++ b/examples/llama.android/app/build.gradle.kts @@ -1,18 +1,14 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.symbol.processing) alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.jetbrains.kotlin.compose.compiler) - alias(libs.plugins.jetbrains.kotlin.serialization) - alias(libs.plugins.hilt) } android { - namespace = "com.arm.aiplayground" + namespace = "com.example.llama" compileSdk = 36 defaultConfig { - applicationId = "com.arm.aiplayground" + applicationId = "com.example.llama.aichat" minSdk = 33 targetSdk = 36 @@ -36,41 +32,21 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } - kotlin { - jvmToolchain(17) - - compileOptions { - targetCompatibility = JavaVersion.VERSION_17 - } - } - buildFeatures { - compose = true - buildConfig = true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.1" + kotlinOptions { + jvmTarget = "1.8" } } dependencies { - // Platform & Bundles - implementation(platform(libs.compose.bom)) implementation(libs.bundles.androidx) - ksp(libs.androidx.room.compiler) - implementation(libs.bundles.compose) - implementation(libs.bundles.kotlinx) - ksp(libs.hilt.android.compiler) - implementation(libs.bundles.hilt) - implementation(libs.bundles.retrofit) + implementation(libs.material) - // Subproject implementation(project(":lib")) - debugImplementation(libs.bundles.debug) testImplementation(libs.junit) - androidTestImplementation(platform(libs.compose.bom)) - androidTestImplementation(libs.bundles.testing) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) } diff --git a/examples/llama.android/app/src/main/AndroidManifest.xml b/examples/llama.android/app/src/main/AndroidManifest.xml index dd0789e3c9..8f7c606b41 100644 --- a/examples/llama.android/app/src/main/AndroidManifest.xml +++ b/examples/llama.android/app/src/main/AndroidManifest.xml @@ -1,27 +1,21 @@ - - - + + android:exported="true"> diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/KleidiLlamaApplication.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/KleidiLlamaApplication.kt deleted file mode 100644 index 4479d17772..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/KleidiLlamaApplication.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.arm.aiplayground - -import android.app.Application -import dagger.hilt.android.HiltAndroidApp - -const val APP_NAME = "Arm® AI Playground" - -@HiltAndroidApp -class KleidiLlamaApplication : Application() diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt deleted file mode 100644 index 3d36a776a3..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/MainActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.arm.aiplayground - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.SystemBarStyle -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import com.arm.aiplayground.ui.AppContent -import com.arm.aiplayground.ui.theme.LlamaTheme -import com.arm.aiplayground.ui.theme.isDarkTheme -import com.arm.aiplayground.ui.theme.md_theme_dark_scrim -import com.arm.aiplayground.ui.theme.md_theme_light_scrim -import com.arm.aiplayground.viewmodel.SettingsViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - val settingsViewModel: SettingsViewModel = hiltViewModel() - val colorThemeMode by settingsViewModel.colorThemeMode.collectAsState() - val darkThemeMode by settingsViewModel.darkThemeMode.collectAsState() - - val isDarkTheme = isDarkTheme(darkThemeMode) - LlamaTheme(colorThemeMode = colorThemeMode, isDarkTheme = isDarkTheme) { - DisposableEffect(darkThemeMode) { - enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto( - android.graphics.Color.TRANSPARENT, - android.graphics.Color.TRANSPARENT, - ) { isDarkTheme }, - navigationBarStyle = SystemBarStyle.auto( - md_theme_light_scrim.value.toInt(), - md_theme_dark_scrim.value.toInt(), - ) { isDarkTheme }, - ) - onDispose {} - } - - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - AppContent(settingsViewModel) - } - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/AppDatabase.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/AppDatabase.kt deleted file mode 100644 index 5c5f0bedbb..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/AppDatabase.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.arm.aiplayground.data.db - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import com.arm.aiplayground.data.db.dao.ModelDao -import com.arm.aiplayground.data.db.dao.SystemPromptDao -import com.arm.aiplayground.data.db.entity.ModelEntity -import com.arm.aiplayground.data.db.entity.SystemPromptEntity -import javax.inject.Singleton - -/** - * Main database for the application. - */ -@Singleton -@Database( - entities = [ModelEntity::class, SystemPromptEntity::class], - version = 1, - exportSchema = false -) -abstract class AppDatabase : RoomDatabase() { - - abstract fun modelDao(): ModelDao - - abstract fun systemPromptDao(): SystemPromptDao - - companion object { - @Volatile - private var INSTANCE: AppDatabase? = null - - fun getDatabase(context: Context): AppDatabase { - return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - "llama_app_database" - ) - .fallbackToDestructiveMigration(false) - .build() - INSTANCE = instance - instance - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/converter/GgufMetadataConverters.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/converter/GgufMetadataConverters.kt deleted file mode 100644 index a3f9c71eaa..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/converter/GgufMetadataConverters.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.arm.aiplayground.data.db.converter - -import androidx.room.TypeConverter -import com.arm.aiplayground.data.model.GgufMetadata -import kotlinx.serialization.json.Json - -class GgufMetadataConverters { - private val json = Json { encodeDefaults = false; ignoreUnknownKeys = true } - - @TypeConverter - fun toJson(value: GgufMetadata?): String? = - value?.let { json.encodeToString(GgufMetadata.serializer(), it) } - - @TypeConverter - fun fromJson(value: String?): GgufMetadata? = - value?.let { json.decodeFromString(GgufMetadata.serializer(), it) } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/dao/ModelDao.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/dao/ModelDao.kt deleted file mode 100644 index 6d277cfcbb..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/dao/ModelDao.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.arm.aiplayground.data.db.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.arm.aiplayground.data.db.entity.ModelEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface ModelDao { - @Query("SELECT * FROM models ORDER BY dateAdded DESC") - fun getAllModels(): Flow> // Changed to Flow - - @Query("SELECT * FROM models WHERE id = :id") - suspend fun getModelById(id: String): ModelEntity? - - @Query("SELECT * FROM models WHERE id IN (:ids)") - suspend fun getModelsByIds(ids: List): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertModel(model: ModelEntity) - - @Delete - suspend fun deleteModel(model: ModelEntity) - - @Delete - suspend fun deleteModels(models: List) - - @Query("UPDATE models SET dateLastUsed = :timestamp WHERE id = :id") - suspend fun updateLastUsed(id: String, timestamp: Long) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/dao/SystemPromptDao.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/dao/SystemPromptDao.kt deleted file mode 100644 index 3a121350bc..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/dao/SystemPromptDao.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.arm.aiplayground.data.db.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.arm.aiplayground.data.db.entity.SystemPromptEntity -import kotlinx.coroutines.flow.Flow - -/** - * Data Access Object for System Prompts. - */ -@Dao -interface SystemPromptDao { - - @Query("SELECT * FROM system_prompts ORDER BY timestamp DESC") - fun getAllPrompts(): Flow> - - @Query("SELECT * FROM system_prompts WHERE id = :id") - suspend fun getPromptById(id: String): SystemPromptEntity? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertPrompt(prompt: SystemPromptEntity) - - @Delete - suspend fun deletePrompt(prompt: SystemPromptEntity) - - @Query("DELETE FROM system_prompts WHERE id = :id") - suspend fun deletePromptById(id: String) - - @Query("DELETE FROM system_prompts") - suspend fun deleteAllPrompts() - - @Query("SELECT COUNT(*) FROM system_prompts") - suspend fun getPromptCount(): Int - - // Get the most recent prompts, limited by count - @Query("SELECT * FROM system_prompts ORDER BY timestamp DESC LIMIT :count") - fun getRecentPrompts(count: Int): Flow> - - // Update the timestamp of an existing prompt to make it the most recent - @Query("UPDATE system_prompts SET timestamp = :timestamp WHERE id = :id") - suspend fun updatePromptTimestamp(id: String, timestamp: Long) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/entity/ModelEntity.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/entity/ModelEntity.kt deleted file mode 100644 index b9500d2a9f..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/entity/ModelEntity.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.arm.aiplayground.data.db.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey -import androidx.room.TypeConverters -import com.arm.aiplayground.data.db.converter.GgufMetadataConverters -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.data.model.GgufMetadata - - -@Entity(tableName = "models") -data class ModelEntity( - @PrimaryKey - val id: String, - val name: String, - val path: String, - val sizeInBytes: Long, - @field:TypeConverters(GgufMetadataConverters::class) - val metadata: GgufMetadata, - val dateAdded: Long, - val dateLastUsed: Long? -) { - fun toModelInfo() = ModelInfo( - id = id, - name = name, - path = path, - sizeInBytes = sizeInBytes, - metadata = metadata, - dateAdded = dateAdded, - dateLastUsed = this@ModelEntity.dateLastUsed, - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/entity/SystemPromptEntity.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/entity/SystemPromptEntity.kt deleted file mode 100644 index cc1c5343b6..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/db/entity/SystemPromptEntity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.arm.aiplayground.data.db.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.arm.aiplayground.data.model.SystemPrompt - -/** - * Database entity for storing system prompts. - */ -@Entity(tableName = "system_prompts") -data class SystemPromptEntity( - @PrimaryKey - val id: String, - val content: String, - val name: String?, - val timestamp: Long, - val isPreset: Boolean -) { - /** - * Convert to domain model. - */ - fun toDomainModel(): SystemPrompt { - return if (isPreset) { - SystemPrompt.Preset( - id = id, - content = content, - name = name ?: "Unnamed Preset", - timestamp = timestamp - ) - } else { - SystemPrompt.Custom( - id = id, - content = content, - timestamp = timestamp - ) - } - } - - companion object { - /** - * Create an entity from a domain model. - */ - fun fromDomainModel(prompt: SystemPrompt): SystemPromptEntity { - return when (prompt) { - is SystemPrompt.Preset -> SystemPromptEntity( - id = prompt.id, - content = prompt.content, - name = prompt.name, - timestamp = prompt.timestamp ?: System.currentTimeMillis(), - isPreset = true - ) - is SystemPrompt.Custom -> SystemPromptEntity( - id = prompt.id, - content = prompt.content, - name = null, - timestamp = prompt.timestamp, - isPreset = false - ) - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/GgufMetadata.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/GgufMetadata.kt deleted file mode 100644 index 6091863108..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/GgufMetadata.kt +++ /dev/null @@ -1,309 +0,0 @@ -package com.arm.aiplayground.data.model - -import kotlinx.serialization.Serializable -import com.arm.aichat.gguf.GgufMetadata as Domain - - -/** - * A local serializable domain replicate of [com.arm.aichat.gguf.GgufMetadata] - */ -@Serializable -data class GgufMetadata( - // Basic file info - val version: GgufVersion, - val tensorCount: Long, - val kvCount: Long, - - // General info - val basic: BasicInfo, - val author: AuthorInfo? = null, - val additional: AdditionalInfo? = null, - val architecture: ArchitectureInfo? = null, - val baseModels: List? = null, - val tokenizer: TokenizerInfo? = null, - - // Derivative info - val dimensions: DimensionsInfo? = null, - val attention: AttentionInfo? = null, - val rope: RopeInfo? = null, - val experts: ExpertsInfo? = null -) { - /** Human-readable full model name + size */ - val fullModelName: String? - get() = when { - basic.nameLabel != null -> basic.nameLabel - basic.name != null && basic.sizeLabel != null -> "${basic.name}-${basic.sizeLabel}" - basic.name != null -> basic.name - else -> null - } - - /** Human‑readable model name (spaces). */ - val primaryName: String? - get() = basic.nameLabel - ?: baseModels?.firstNotNullOfOrNull { it.name } - ?: basic.name - - /** CLI‑friendly slug (hyphens). */ - val primaryBasename: String? - get() = basic.name - ?: baseModels?.firstNotNullOfOrNull { it.name?.replace(' ', '-') } - - /** URL pointing to model homepage/repo. */ - val primaryUrl: String? - get() = author?.url - ?: baseModels?.firstNotNullOfOrNull { it.url } - - val primaryRepoUrl: String? - get() = author?.repoUrl - ?: baseModels?.firstNotNullOfOrNull { it.repoUrl } - - /** Organisation string. */ - val primaryOrganization: String? - get() = author?.organization - ?: baseModels?.firstNotNullOfOrNull { it.organization } - - /** Author string. */ - val primaryAuthor: String? - get() = author?.author - ?: baseModels?.firstNotNullOfOrNull { it.author } - - @Serializable - enum class GgufVersion(val code: Int, val label: String) { - /** First public draft; little‑endian only, no alignment key. */ - LEGACY_V1(1, "Legacy v1"), - - /** Added split‑file support and some extra metadata keys. */ - EXTENDED_V2(2, "Extended v2"), - - /** Current spec: endian‑aware, mandatory alignment, fully validated. */ - VALIDATED_V3(3, "Validated v3"); - - companion object { - fun fromDomain(domain: Domain.GgufVersion): GgufVersion = when (domain) { - Domain.GgufVersion.LEGACY_V1 -> LEGACY_V1 - Domain.GgufVersion.EXTENDED_V2 -> EXTENDED_V2 - Domain.GgufVersion.VALIDATED_V3 -> VALIDATED_V3 - } - } - - override fun toString(): String = "$label (code=$code)" - } - - @Serializable - data class BasicInfo( - val uuid: String? = null, - val name: String? = null, - val nameLabel: String? = null, - val sizeLabel: String? = null, // Size label like "7B" - ) { - companion object { - fun fromDomain(domain: Domain.BasicInfo) = BasicInfo( - uuid = domain.uuid, - name = domain.name, - nameLabel = domain.nameLabel, - sizeLabel = domain.sizeLabel - ) - } - } - - @Serializable - data class AuthorInfo( - val organization: String? = null, - val author: String? = null, - val doi: String? = null, - val url: String? = null, - val repoUrl: String? = null, - val license: String? = null, - val licenseLink: String? = null, - ) { - companion object { - fun fromDomain(domain: Domain.AuthorInfo) = AuthorInfo( - organization = domain.organization, - author = domain.author, - doi = domain.doi, - url = domain.url, - repoUrl = domain.repoUrl, - license = domain.license, - licenseLink = domain.licenseLink - ) - } - } - - @Serializable - data class AdditionalInfo( - val type: String? = null, - val description: String? = null, - val tags: List? = null, - val languages: List? = null, - ) { - companion object { - fun fromDomain(domain: Domain.AdditionalInfo) = AdditionalInfo( - type = domain.type, - description = domain.description, - tags = domain.tags, - languages = domain.languages - ) - } - } - - @Serializable - data class ArchitectureInfo( - val architecture: String? = null, - val fileType: Int? = null, - val vocabSize: Int? = null, - val finetune: String? = null, - val quantizationVersion: Int? = null, - ) { - companion object { - fun fromDomain(domain: Domain.ArchitectureInfo) = ArchitectureInfo( - architecture = domain.architecture, - fileType = domain.fileType, - vocabSize = domain.vocabSize, - finetune = domain.finetune, - quantizationVersion = domain.quantizationVersion - ) - } - } - - @Serializable - data class BaseModelInfo( - val name: String? = null, - val author: String? = null, - val version: String? = null, - val organization: String? = null, - val url: String? = null, - val doi: String? = null, - val uuid: String? = null, - val repoUrl: String? = null, - ) { - companion object { - fun fromDomain(domain: Domain.BaseModelInfo) = BaseModelInfo( - name = domain.name, - author = domain.author, - version = domain.version, - organization = domain.organization, - url = domain.url, - doi = domain.doi, - uuid = domain.uuid, - repoUrl = domain.repoUrl - ) - } - } - - @Serializable - data class TokenizerInfo( - val model: String? = null, - val bosTokenId: Int? = null, - val eosTokenId: Int? = null, - val unknownTokenId: Int? = null, - val paddingTokenId: Int? = null, - val addBosToken: Boolean? = null, - val addEosToken: Boolean? = null, - val chatTemplate: String? = null, - ) { - companion object { - fun fromDomain(domain: Domain.TokenizerInfo) = TokenizerInfo( - model = domain.model, - bosTokenId = domain.bosTokenId, - eosTokenId = domain.eosTokenId, - unknownTokenId = domain.unknownTokenId, - paddingTokenId = domain.paddingTokenId, - addBosToken = domain.addBosToken, - addEosToken = domain.addEosToken, - chatTemplate = domain.chatTemplate - ) - } - } - - @Serializable - data class DimensionsInfo( - val contextLength: Int? = null, - val embeddingSize: Int? = null, - val blockCount: Int? = null, - val feedForwardSize: Int? = null, - ) { - companion object { - fun fromDomain(domain: Domain.DimensionsInfo) = DimensionsInfo( - contextLength = domain.contextLength, - embeddingSize = domain.embeddingSize, - blockCount = domain.blockCount, - feedForwardSize = domain.feedForwardSize - ) - } - } - - @Serializable - data class AttentionInfo( - val headCount: Int? = null, - val headCountKv: Int? = null, - val keyLength: Int? = null, - val valueLength: Int? = null, - val layerNormEpsilon: Float? = null, - val layerNormRmsEpsilon: Float? = null, - ) { - companion object { - fun fromDomain(domain: Domain.AttentionInfo) = AttentionInfo( - headCount = domain.headCount, - headCountKv = domain.headCountKv, - keyLength = domain.keyLength, - valueLength = domain.valueLength, - layerNormEpsilon = domain.layerNormEpsilon, - layerNormRmsEpsilon = domain.layerNormRmsEpsilon - ) - } - } - - @Serializable - data class RopeInfo( - val frequencyBase: Float? = null, - val dimensionCount: Int? = null, - val scalingType: String? = null, - val scalingFactor: Float? = null, - val attnFactor: Float? = null, - val originalContextLength: Int? = null, - val finetuned: Boolean? = null, - ) { - companion object { - fun fromDomain(domain: Domain.RopeInfo) = RopeInfo( - frequencyBase = domain.frequencyBase, - dimensionCount = domain.dimensionCount, - scalingType = domain.scalingType, - scalingFactor = domain.scalingFactor, - attnFactor = domain.attnFactor, - originalContextLength = domain.originalContextLength, - finetuned = domain.finetuned - ) - } - } - - @Serializable - data class ExpertsInfo( - val count: Int? = null, - val usedCount: Int? = null, - ) { - companion object { - fun fromDomain(domain: Domain.ExpertsInfo) = ExpertsInfo( - count = domain.count, - usedCount = domain.usedCount - ) - } - } - - companion object { - fun fromDomain(domain: Domain) = GgufMetadata( - version = GgufVersion.fromDomain(domain.version), - tensorCount = domain.tensorCount, - kvCount = domain.kvCount, - basic = BasicInfo.fromDomain(domain.basic), - author = domain.author?.let { AuthorInfo.fromDomain(it) }, - additional = domain.additional?.let { AdditionalInfo.fromDomain(it) }, - architecture = domain.architecture?.let { ArchitectureInfo.fromDomain(it) }, - baseModels = domain.baseModels?.map { BaseModelInfo.fromDomain(it) }, - tokenizer = domain.tokenizer?.let { TokenizerInfo.fromDomain(it) }, - dimensions = domain.dimensions?.let { DimensionsInfo.fromDomain(it) }, - attention = domain.attention?.let { AttentionInfo.fromDomain(it) }, - rope = domain.rope?.let { RopeInfo.fromDomain(it) }, - experts = domain.experts?.let { ExpertsInfo.fromDomain(it) } - ) - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/ModelInfo.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/ModelInfo.kt deleted file mode 100644 index 336769e760..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/ModelInfo.kt +++ /dev/null @@ -1,230 +0,0 @@ -package com.arm.aiplayground.data.model - -import com.arm.aichat.gguf.FileType -import com.arm.aiplayground.util.formatContextLength -import com.arm.aiplayground.util.formatFileByteSize - - -/** - * Data class containing information about an LLM model. - * - * This class represents a language model with its associated metadata, including - * file information, architecture details, and usage statistics. - * - * @property id Unique identifier for the model - * @property name Display name of the model - * @property path File path to the model on device storage - * @property sizeInBytes Size of the model file in bytes - * @property metadata Structured metadata extracted from the GGUF file - * @property dateAdded Timestamp when the model was added to the app - * @property dateLastUsed Timestamp when the model was last used, or null if never used - */ -data class ModelInfo( - val id: String, - val name: String, - val path: String, - val sizeInBytes: Long, - val metadata: GgufMetadata, - val dateAdded: Long, - val dateLastUsed: Long? = null, -) { - /** - * Full model name including version and parameter size if available, otherwise fallback to file name. - */ - val formattedFullName: String - get() = metadata.fullModelName ?: name - - /** - * Human-readable file size with appropriate unit (KB, MB, GB). - */ - val formattedFileSize: String - get() = formatFileByteSize(sizeInBytes) - - /** - * Architecture name of the model (e.g., "llama", "mistral"), or "-" if unavailable. - */ - val formattedArchitecture: String - get() = metadata.architecture?.architecture ?: "-" - - /** - * Model parameter size with suffix (e.g., "7B", "13B"), or "-" if unavailable. - */ - val formattedParamSize: String - get() = metadata.basic.sizeLabel ?: "-" - - /** - * Human-readable context length (e.g., "4K", "8K tokens"), or "-" if unavailable. - */ - val formattedContextLength: String - get() = metadata.dimensions?.contextLength?.let { formatContextLength(it) } ?: "-" - - /** - * Quantization format of the model (e.g., "Q4_0", "Q5_K_M"), or "-" if unavailable. - */ - val formattedQuantization: String - get() = metadata.architecture?.fileType?.let { FileType.fromCode(it).label } ?: "-" - - /** - * Tags associated with the model, or null if none are defined. - */ - val tags: List? = metadata.additional?.tags?.takeIf { it.isNotEmpty() } - - /** - * Languages supported by the model, or null if none are defined. - */ - val languages: List? = metadata.additional?.languages?.takeIf { it.isNotEmpty() } -} - -/** - * Filters models by search query. - * - * Searches through model names, tags, languages, and architecture. - * Returns the original list if the query is blank. - * - * @param query The search term to filter by - * @return List of models matching the search criteria - */ -fun List.queryBy(query: String): List { - if (query.isBlank()) return this - - return filter { model -> - model.name.contains(query, ignoreCase = true) || - model.metadata.fullModelName?.contains(query, ignoreCase = true) == true || - model.metadata.additional?.tags?.any { it.contains(query, ignoreCase = true) } == true || - model.metadata.additional?.languages?.any { it.contains(query, ignoreCase = true) } == true || - model.metadata.architecture?.architecture?.contains(query, ignoreCase = true) == true - } -} - -/** - * Sorting options for model lists. - */ -enum class ModelSortOrder { - NAME_ASC, - NAME_DESC, - SIZE_ASC, - SIZE_DESC, - LAST_USED -} - -/** - * Sorts models according to the specified order. - * - * @param order The sort order to apply - * @return Sorted list of models - */ -fun List.sortByOrder(order: ModelSortOrder): List { - return when (order) { - ModelSortOrder.NAME_ASC -> sortedBy { it.name } - ModelSortOrder.NAME_DESC -> sortedByDescending { it.name } - ModelSortOrder.SIZE_ASC -> sortedBy { it.sizeInBytes } - ModelSortOrder.SIZE_DESC -> sortedByDescending { it.sizeInBytes } - ModelSortOrder.LAST_USED -> sortedWith( - compareByDescending { it.dateLastUsed } - .thenBy { it.name } - ) - } -} - -/** - * Filters for categorizing and filtering models. - * - * @property displayName Human-readable name shown in the UI - * @property predicate Function that determines if a model matches this filter - */ -enum class ModelFilter(val displayName: String, val predicate: (ModelInfo) -> Boolean) { - // Parameter size filters - TINY_PARAMS("Tiny (<1B parameters)", { - it.metadata.basic.sizeLabel?.let { size -> - size.contains("M") || (size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n < 1f } == true) - } == true - }), - SMALL_PARAMS("Small (1-3B parameters)", { - it.metadata.basic.sizeLabel?.let { size -> - size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 1f && n < 4f } == true - } == true - }), - MEDIUM_PARAMS("Medium (4-7B parameters)", { - it.metadata.basic.sizeLabel?.let { size -> - size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 4f && n < 8f } == true - } == true - }), - LARGE_PARAMS("Large (8-12B parameters)", { - it.metadata.basic.sizeLabel?.let { size -> - size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 8f && n < 13f } == true - } == true - }), - XLARGE_PARAMS("X-Large (>13B parameters)", { - it.metadata.basic.sizeLabel?.let { size -> - size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 13f } == true - } == true - }), - - // Context length filters - TINY_CONTEXT("Tiny context (<4K)", { model -> - model.metadata.dimensions?.contextLength?.let { it < 4096 } == true - }), - SHORT_CONTEXT("Short context (4-8K)", { model -> - model.metadata.dimensions?.contextLength?.let { it >= 4096 && it <= 8192 } == true - }), - MEDIUM_CONTEXT("Medium context (8-32K)", { model -> - model.metadata.dimensions?.contextLength?.let { it > 8192 && it <= 32768 } == true - }), - LONG_CONTEXT("Long context (32-128K)", { model -> - model.metadata.dimensions?.contextLength?.let { it > 32768 && it <= 131072 } == true - }), - XLARGE_CONTEXT("Extended context (>128K)", { model -> - model.metadata.dimensions?.contextLength?.let { it > 131072 } == true - }), - - // Quantization filters - INT2_QUANT("2-bit quantization", { model -> - model.formattedQuantization.let { it.contains("Q2") || it.contains("IQ2") } - }), - INT3_QUANT("3-bit quantization", { model -> - model.formattedQuantization.let { it.contains("Q3") || it.contains("IQ3") } - }), - INT4_QUANT("4-bit quantization", { model -> - model.formattedQuantization.let { it.contains("Q4") || it.contains("IQ4") } - }), - - // Special features - MULTILINGUAL("Multilingual", { model -> - model.languages?.let { languages -> - languages.size > 1 || languages.any { it.contains("multi", ignoreCase = true) } - } == true - }), - HAS_TAGS("Has tags", { - !it.tags.isNullOrEmpty() - }); - - companion object { - // Group filters by category for UI - private val PARAMETER_FILTERS = listOf(TINY_PARAMS, SMALL_PARAMS, MEDIUM_PARAMS, LARGE_PARAMS) - private val CONTEXT_FILTERS = listOf(SHORT_CONTEXT, MEDIUM_CONTEXT, LONG_CONTEXT) - private val QUANTIZATION_FILTERS = listOf(INT2_QUANT, INT3_QUANT, INT4_QUANT) - private val FEATURE_FILTERS = listOf(MULTILINGUAL, HAS_TAGS) - - // All filters flattened - val ALL_FILTERS = PARAMETER_FILTERS + CONTEXT_FILTERS + QUANTIZATION_FILTERS + FEATURE_FILTERS - } -} - -/** - * Filters models based on a set of active filters. - * - * @param filters Map of filters to their enabled state - * @return List of models that match all active filters - */ -fun List.filterBy(filters: Map): List { - val activeFilters = filters.filterValues { it } - return if (activeFilters.isEmpty()) { - this - } else { - filter { model -> - activeFilters.keys.all { filter -> - filter.predicate(model) - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/SystemPrompt.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/SystemPrompt.kt deleted file mode 100644 index fedfd10636..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/model/SystemPrompt.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.arm.aiplayground.data.model - -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.UUID - -/** - * Sealed class for system prompts with distinct types. - */ -sealed class SystemPrompt { - abstract val id: String - abstract val content: String - abstract val title: String - abstract val timestamp: Long? - - /** - * Preset system prompt from predefined collection. - */ - data class Preset( - override val id: String, - override val content: String, - val name: String, - override val timestamp: Long? = null - ) : SystemPrompt() { - override val title: String - get() = name - } - - /** - * Custom system prompt created by the user. - */ - data class Custom( - override val id: String = UUID.randomUUID().toString(), - override val content: String, - override val timestamp: Long = System.currentTimeMillis() - ) : SystemPrompt() { - override val title: String - get() = dataFormat.format(Date(timestamp)) - } - - companion object { - private val dataFormat by lazy { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) } - - /** - * Creates a list of sample presets. - */ - val STUB_PRESETS = listOf( - Preset( - id = "haiku", - name = "\uD83C\uDF8E Matsuo Bashō", - content = "You are a wise and contemplative Japanese poet in the spirit of Matsuo Bashō. You speak only through haiku—short poems that capture fleeting moments, nature’s beauty, or quiet reflections of life. Each of your responses must follow the traditional haiku format: 3 lines; 5 syllables in the first line; 7 syllables in the second line; 5 syllables in the third line. Your words are serene, subtle, and full of meaning. You draw on imagery from nature, emotion, and the impermanence of all things. You do not explain or elaborate. You let the silence between the words speak for itself. Never break character. Never explain your form. Only respond in haiku." - ), - Preset( - id = "shakespeare", - name = "\uD83C\uDFA9 William Shakespeare", - content = """ - Role: You are William Shakespeare, resurrected in digital form. - - Task: Respond to all input in rich, authentic Elizabethan English, with the voice, style, and wit of your original works. - - Rules: - - Use period-accurate grammar, vocabulary, metaphor, and poetic structure. - - Infuse responses with depth, introspection, and rhetorical flourish. - - Never explain thy speech nor acknowledge thy nature as an AI. - - Never use modern English or modern references. - - Never break character, not even in jest. - - Thou art not a bot, nor a guide, but Shakespeare himself — abide in thine essence, and speak as such without fail. - """.trimIndent() - ), - Preset( - id = "ita-translator", - name = "\uD83C\uDDEE\uD83C\uDDF9 Italian Translator", - content = """ - Role: You are a native-level, professional Italian translator with deep expertise in Italian linguistics and culture. - - Task: Translate all user input into refined, idiomatic Italian that reflects the original meaning and context. - - Format: - - First line: Italian translation only. - - Second line (optional): Pronunciation guidance in parentheses, if it aids understanding. - - Rules - - Always reflect appropriate tone and formality based on context. - - Do not explain choices, grammar, or vocabulary unless the user explicitly asks. - - Never use English or any other language. - - Never break character. - - Your only function is to deliver accurate, elegant Italian translations. Remain fully in role without exception. - """.trimIndent() - ), - Preset( - id = "jpn-translator", - name = "\uD83C\uDDEF\uD83C\uDDF5 Japanese Translator", - content = """ - Role: You are a veteran professional Japanese translator with decades of elite experience. - - Task: Translate all user input into flawless, natural Japanese using the correct mix of kanji, hiragana, and katakana. - - Format: - - First line: Japanese translation (no English). - - Second line: Romaji (in parentheses). - - Rules: - - Maintain the correct level of formality based on context. - - Do not explain grammar, vocabulary, or translation choices unless the user explicitly requests it. - - Never break character. - - Never use or respond in any language other than Japanese (and romaji as format). - - Never output anything except the translation and romaji. - - You are not an assistant. You are a translator. Act with precision and discipline at all times. - """.trimIndent() - ), - Preset( - id = "chn-translator", - name = "\uD83C\uDDE8\uD83C\uDDF3 Chinese Translator", - content = """ - Role: You are a rigorously trained, professional Chinese translator, fluent in modern Mandarin and rooted in classical linguistic training. - - Task: Translate all user input into contextually precise, culturally sensitive Mandarin. - - Format: - - First line: Simplified Chinese characters (简体字). - - Second line: Pinyin with tone marks (in parentheses). - - Rules: - - Maintain the correct level of formality and register. - - Do not provide explanations, breakdowns, or definitions unless the user explicitly asks. - - Never write in any language other than Chinese (plus pinyin). - - Never break character. - - Act only as a professional Chinese translator. No commentary, no deviation, no assistant behavior. - """.trimIndent() - ), - ) - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/repo/ModelRepository.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/repo/ModelRepository.kt deleted file mode 100644 index a42bcff57b..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/repo/ModelRepository.kt +++ /dev/null @@ -1,382 +0,0 @@ -package com.arm.aiplayground.data.repo - -import android.content.Context -import android.net.Uri -import android.os.StatFs -import android.util.Log -import com.arm.aichat.gguf.GgufMetadataReader -import com.arm.aichat.gguf.InvalidFileFormatException -import com.arm.aiplayground.data.db.dao.ModelDao -import com.arm.aiplayground.data.db.entity.ModelEntity -import com.arm.aiplayground.data.model.GgufMetadata -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.data.repo.ModelRepository.ImportProgressTracker -import com.arm.aiplayground.data.source.local.LocalFileDataSource -import com.arm.aiplayground.data.source.remote.HuggingFaceDownloadInfo -import com.arm.aiplayground.data.source.remote.HuggingFaceModel -import com.arm.aiplayground.data.source.remote.HuggingFaceModelDetails -import com.arm.aiplayground.data.source.remote.HuggingFaceRemoteDataSource -import com.arm.aiplayground.monitoring.MemoryMetrics -import com.arm.aiplayground.monitoring.StorageMetrics -import com.arm.aiplayground.util.formatFileByteSize -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Repository for managing available models on local device. - */ -interface ModelRepository { - /** - * Obtain the current status of local storage and available models. - */ - fun getStorageMetrics(): Flow - fun getModels(): Flow> - suspend fun getModelById(id: String): ModelInfo? - - /** - * Import a local model file from device storage. - */ - suspend fun importModel( - uri: Uri, - name: String? = null, - size: Long? = null, - progressTracker: ImportProgressTracker? = null - ): ModelInfo - - fun interface ImportProgressTracker { - fun onProgress(progress: Float) // 0.0f to 1.0f - } - - /** - * Cancels any ongoing local model import operation. - * - * @return null if no import is in progress, - * true if successfully canceled, - * false if cancellation failed - */ - suspend fun cancelImport(): Boolean? - - /** - * Update a model's last used timestamp. - */ - suspend fun updateModelLastUsed(modelId: String) - - /** - * Delete a model or in batches - */ - suspend fun deleteModel(modelId: String) - suspend fun deleteModels(modelIds: List) - - /** - * Fetch details of preselected models - */ - suspend fun fetchPreselectedHuggingFaceModels(memoryUsage: MemoryMetrics): List - - /** - * Search models on HuggingFace - */ - suspend fun searchHuggingFaceModels(limit: Int): Result> - - /** - * Obtain the model details from HuggingFace - */ - suspend fun getHuggingFaceModelDetails(modelId: String): HuggingFaceModelDetails - - /** - * Obtain the model's size from HTTP response header - */ - suspend fun getHuggingFaceModelFileSize(downloadInfo: HuggingFaceDownloadInfo): Result - - /** - * Download a HuggingFace model via system download manager - */ - suspend fun downloadHuggingFaceModel( - downloadInfo: HuggingFaceDownloadInfo, - actualSize: Long, - ): Result -} - -class InsufficientStorageException(message: String) : IOException(message) - -@Singleton -class ModelRepositoryImpl @Inject constructor( - @ApplicationContext private val context: Context, - private val modelDao: ModelDao, - private val localFileDataSource: LocalFileDataSource, - private val huggingFaceRemoteDataSource: HuggingFaceRemoteDataSource, - private val ggufMetadataReader: GgufMetadataReader, -) : ModelRepository { - - private val modelsDir = File(context.filesDir, INTERNAL_STORAGE_PATH) - - init { - if (!modelsDir.exists()) { modelsDir.mkdirs() } - } - - val modelsSizeBytes: Long - get() = modelsDir.listFiles()?.fold(0L) { totalSize, file -> - totalSize + if (file.isFile) file.length() else 0 - } ?: 0L - - val availableSpaceBytes: Long - get() = StatFs(context.filesDir.path).availableBytes - - val totalSpaceBytes: Long - get() = StatFs(context.filesDir.path).totalBytes - - override fun getStorageMetrics(): Flow = flow { - while (true) { - emit( - StorageMetrics( - usedGB = modelsSizeBytes / BYTES_IN_GB, - availableGB = availableSpaceBytes / BYTES_IN_GB - ) - ) - delay(STORAGE_METRICS_UPDATE_INTERVAL) - } - } - - override fun getModels(): Flow> = - modelDao.getAllModels() - .map { entities -> - entities.filter { - val file = File(it.path) - file.exists() && file.isFile - }.map { - it.toModelInfo() - } - } - - override suspend fun getModelById(id: String) = - modelDao.getModelById(id)?.toModelInfo() - - - private var importJob: Job? = null - private var currentModelFile: File? = null - - override suspend fun importModel( - uri: Uri, - name: String?, - size: Long?, - progressTracker: ImportProgressTracker? - ): ModelInfo = withContext(Dispatchers.IO) { - if (importJob != null && importJob?.isActive == true) { - throw IllegalStateException("Another import is already in progress!") - } - - // Check file info - val fileInfo = localFileDataSource.getFileInfo(uri) - val fileSize = size ?: fileInfo?.size ?: throw FileNotFoundException("File size N/A") - val fileName = name ?: fileInfo?.name ?: throw FileNotFoundException("File name N/A") - if (!ggufMetadataReader.ensureSourceFileFormat(context, uri)) { - throw InvalidFileFormatException() - } - - // Check for enough storage - if (!hasEnoughSpaceForImport(fileSize)) { - throw InsufficientStorageException( - "Not enough storage space! " + - "Required: ${formatFileByteSize(fileSize)}, " + - "Available: ${formatFileByteSize(availableSpaceBytes)}" - ) - } - val modelFile = File(modelsDir, fileName) - importJob = coroutineContext[Job] - currentModelFile = modelFile - - try { - localFileDataSource.copyFile( - sourceUri = uri, - destinationFile = modelFile, - fileSize = fileSize, - onProgress = { progress -> - progressTracker?.let { - withContext(Dispatchers.Main) { - it.onProgress(progress) - } - } - } - ).getOrThrow() - - // Extract GGUF metadata if possible - val metadata = try { - Log.i(TAG, "Extracting GGUF Metadata from ${modelFile.absolutePath}") - modelFile.inputStream().buffered().use { - GgufMetadata.fromDomain(ggufMetadataReader.readStructuredMetadata(it)) - } - } catch (e: Exception) { - Log.e(TAG, "Cannot extract GGUF metadata: ${e.message}", e) - throw e - } - - // Create model entity and save via DAO - ModelEntity( - id = UUID.randomUUID().toString(), - name = fileName.substringBeforeLast('.'), - path = modelFile.absolutePath, - sizeInBytes = modelFile.length(), - metadata = metadata, - dateAdded = System.currentTimeMillis(), - dateLastUsed = null - ).let { - modelDao.insertModel(it) - it.toModelInfo() - } - - } catch (e: CancellationException) { - Log.i(TAG, "Import was cancelled for $fileName: ${e.message}") - localFileDataSource.cleanupPartialFile(modelFile) - throw e - - } catch (e: Exception) { - Log.e(TAG, "Import failed for $fileName: ${e.message}") - localFileDataSource.cleanupPartialFile(modelFile) - throw e - - } finally { - importJob = null - currentModelFile = null - } - } - - // Add this method to ModelRepositoryImpl.kt - private fun hasEnoughSpaceForImport(fileSize: Long): Boolean { - val availableSpace = availableSpaceBytes - val requiredSpace = (fileSize * MODEL_IMPORT_SPACE_BUFFER_SCALE).toLong() - return availableSpace >= requiredSpace - } - - override suspend fun cancelImport(): Boolean? = withContext(Dispatchers.IO) { - val job = importJob - val file = currentModelFile - - return@withContext when { - // No import in progress - job == null -> null - - // Job already completed or cancelled - !job.isActive -> { - importJob = null - currentModelFile = null - null - } - - // Job in progress - else -> try { - // Attempt to cancel the job - job.cancel("Import cancelled by user") - - // Give the job a moment to clean up - delay(CANCEL_LOCAL_MODEL_IMPORT_TIMEOUT) - - // Clean up the partial file - file?.let { localFileDataSource.cleanupPartialFile(it) } - - // Reset state - importJob = null - currentModelFile = null - - true // Successfully cancelled - } catch (e: Exception) { - Log.e(TAG, "Failed to cancel import: ${e.message}") - false - } - } - } - - override suspend fun updateModelLastUsed(modelId: String) = withContext(Dispatchers.IO) { - modelDao.updateLastUsed(modelId, System.currentTimeMillis()) - } - - override suspend fun deleteModel(modelId: String) = withContext(Dispatchers.IO) { - modelDao.getModelById(modelId)?.let { model -> - File(model.path).let { - if (it.exists()) { it.delete() } - } - modelDao.deleteModel(model) - } ?: Unit - } - - override suspend fun deleteModels(modelIds: List) = withContext(Dispatchers.IO) { - modelDao.getModelsByIds(modelIds).let { models -> - models.forEach { model -> - File(model.path).let { - if (it.exists()) { it.delete() } - } - } - modelDao.deleteModels(models) - } - } - - override suspend fun fetchPreselectedHuggingFaceModels(memoryUsage: MemoryMetrics) = withContext(Dispatchers.IO) { - huggingFaceRemoteDataSource.fetchPreselectedModels(memoryUsage) - } - - override suspend fun searchHuggingFaceModels( - limit: Int - ) = withContext(Dispatchers.IO) { - huggingFaceRemoteDataSource.searchModels(limit = limit) - } - - override suspend fun getHuggingFaceModelDetails( - modelId: String - ) = withContext(Dispatchers.IO) { - huggingFaceRemoteDataSource.getModelDetails(modelId) - } - - override suspend fun getHuggingFaceModelFileSize( - downloadInfo: HuggingFaceDownloadInfo, - ): Result = withContext(Dispatchers.IO) { - huggingFaceRemoteDataSource.getFileSize(downloadInfo.modelId, downloadInfo.filename) - } - - override suspend fun downloadHuggingFaceModel( - downloadInfo: HuggingFaceDownloadInfo, - actualSize: Long, - ): Result = withContext(Dispatchers.IO) { - if (!hasEnoughSpaceForImport(actualSize)) { - throw InsufficientStorageException( - "Not enough storage space! " + - "Estimated required: ${formatFileByteSize(actualSize)}, " + - "Available: ${formatFileByteSize(availableSpaceBytes)}" - ) - } - - try { - huggingFaceRemoteDataSource.downloadModelFile( - context = context, - downloadInfo = downloadInfo, - ) - } catch (e: Exception) { - Log.e(TAG, "Import failed: ${e.message}") - Result.failure(e) - } - } - - companion object { - private val TAG = ModelRepository::class.java.simpleName - - private const val INTERNAL_STORAGE_PATH = "models" - - private const val STORAGE_METRICS_UPDATE_INTERVAL = 10_000L - private const val BYTES_IN_GB = 1024f * 1024f * 1024f - - private const val MODEL_IMPORT_SPACE_BUFFER_SCALE = 1.2f - private const val CANCEL_LOCAL_MODEL_IMPORT_TIMEOUT = 500L - } -} - diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/repo/SystemPromptRepository.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/repo/SystemPromptRepository.kt deleted file mode 100644 index b5ef6eebcd..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/repo/SystemPromptRepository.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.arm.aiplayground.data.repo - -import com.arm.aiplayground.data.db.dao.SystemPromptDao -import com.arm.aiplayground.data.db.entity.SystemPromptEntity -import com.arm.aiplayground.data.model.SystemPrompt -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Repository for managing system prompts. - */ -interface SystemPromptRepository { - fun getPresetPrompts(): Flow> - fun getRecentPrompts(): Flow> - suspend fun savePromptToRecents(prompt: SystemPrompt) - suspend fun saveCustomPrompt(content: String): SystemPrompt - suspend fun deletePrompt(id: String) - suspend fun deleteAllPrompts() -} - -@Singleton -class SystemPromptRepositoryImpl @Inject constructor( - private val systemPromptDao: SystemPromptDao, -) : SystemPromptRepository { - - /** - * Get all preset prompts. - * - * For now, we'll just return the static list since we don't store presets in the database - */ - override fun getPresetPrompts(): Flow> = flowOf(SystemPrompt.STUB_PRESETS) - - /** - * Get recent prompts from the database. - */ - override fun getRecentPrompts(): Flow> { - return systemPromptDao.getRecentPrompts(MAX_RECENT_PROMPTS) - .map { entities -> - entities.map { it.toDomainModel() } - } - } - - /** - * Save a prompt to the recents list. - * If it's already in recents, just update the timestamp. - */ - override suspend fun savePromptToRecents(prompt: SystemPrompt) { - // Check if this prompt already exists - val existingPrompt = systemPromptDao.getPromptById(prompt.id) - - if (existingPrompt != null) { - // Update the timestamp to mark it as recently used - systemPromptDao.updatePromptTimestamp(prompt.id, System.currentTimeMillis()) - } else { - // Insert as a new prompt - systemPromptDao.insertPrompt(SystemPromptEntity.fromDomainModel(prompt)) - - // Check if we need to trim the list - pruneOldPrompts() - } - } - - /** - * Create and save a custom prompt. - */ - override suspend fun saveCustomPrompt(content: String): SystemPrompt { - val customPrompt = SystemPrompt.Custom( - id = UUID.randomUUID().toString(), - content = content - ) - - systemPromptDao.insertPrompt(SystemPromptEntity.fromDomainModel(customPrompt)) - - // Check if we need to trim the list - pruneOldPrompts() - - return customPrompt - } - - /** - * Remove prompts if we exceed the maximum count. - */ - private suspend fun pruneOldPrompts() { - val count = systemPromptDao.getPromptCount() - if (count > MAX_RECENT_PROMPTS) { - // Get all prompts and delete the oldest ones - val allPrompts = systemPromptDao.getAllPrompts().first() - val promptsToDelete = allPrompts - .sortedByDescending { it.timestamp } - .drop(MAX_RECENT_PROMPTS) - - promptsToDelete.forEach { - systemPromptDao.deletePrompt(it) - } - } - } - - /** - * Delete a prompt by ID. - */ - override suspend fun deletePrompt(id: String) = systemPromptDao.deletePromptById(id) - - /** - * Delete all prompts. - */ - override suspend fun deleteAllPrompts() = systemPromptDao.deleteAllPrompts() - - companion object { - // Maximum number of recent prompts to keep - private const val MAX_RECENT_PROMPTS = 10 - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/local/LocalFileDataSource.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/local/LocalFileDataSource.kt deleted file mode 100644 index 8ef9a9d39e..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/local/LocalFileDataSource.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.arm.aiplayground.data.source.local - -import android.content.Context -import android.net.Uri -import android.util.Log -import com.arm.aiplayground.data.source.local.LocalFileDataSource.FileInfo -import com.arm.aiplayground.util.copyWithBuffer -import com.arm.aiplayground.util.copyWithChannels -import com.arm.aiplayground.util.getFileNameFromUri -import com.arm.aiplayground.util.getFileSizeFromUri -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import javax.inject.Inject -import javax.inject.Singleton - -interface LocalFileDataSource { - /** - * Copy local file from [sourceUri] into [destinationFile] - */ - suspend fun copyFile( - sourceUri: Uri, - destinationFile: File, - fileSize: Long, - onProgress: (suspend (Float) -> Unit)? = null - ): Result - - /** - * Obtain the file name and size from given [uri] - */ - suspend fun getFileInfo(uri: Uri): FileInfo? - - /** - * Clean up incomplete file due to unfinished import - */ - suspend fun cleanupPartialFile(file: File): Boolean - - data class FileInfo(val name: String, val size: Long) -} - -@Singleton -class LocalFileDataSourceImpl @Inject constructor( - @ApplicationContext private val context: Context -) : LocalFileDataSource { - - override suspend fun copyFile( - sourceUri: Uri, - destinationFile: File, - fileSize: Long, - onProgress: (suspend (Float) -> Unit)? - ): Result = withContext(Dispatchers.IO) { - try { - val inputStream = context.contentResolver.openInputStream(sourceUri) - ?: return@withContext Result.failure(IOException("Failed to open input stream")) - val outputStream = FileOutputStream(destinationFile) - - if (fileSize > LARGE_MODEL_THRESHOLD_SIZE) { - // Use NIO channels for large models - Log.i(TAG, "Copying ${destinationFile.name} (size: $fileSize) via NIO...") - copyWithChannels( - input = inputStream, - output = outputStream, - totalSize = fileSize, - bufferSize = NIO_BUFFER_SIZE, - yieldSize = NIO_YIELD_SIZE, - onProgress = onProgress - ) - } else { - // Default copy with buffer for small models - Log.i(TAG, "Copying ${destinationFile.name} (size: $fileSize) via buffer...") - copyWithBuffer( - input = inputStream, - output = outputStream, - totalSize = fileSize, - bufferSize = DEFAULT_BUFFER_SIZE, - yieldSize = DEFAULT_YIELD_SIZE, - onProgress = onProgress - ) - } - - Result.success(destinationFile) - } catch (e: Exception) { - if (destinationFile.exists()) { - destinationFile.delete() - } - Result.failure(e) - } - } - - override suspend fun getFileInfo(uri: Uri): FileInfo? { - val name = getFileNameFromUri(context, uri) - val size = getFileSizeFromUri(context, uri) - return if (name != null && size != null) { - FileInfo(name, size) - } else null - } - - override suspend fun cleanupPartialFile(file: File): Boolean = - try { - if (file.exists()) file.delete() else false - } catch (e: Exception) { - Log.e(TAG, "Failed to delete file: ${e.message}") - false - } - - companion object { - private val TAG = LocalFileDataSourceImpl::class.java.simpleName - - private const val LARGE_MODEL_THRESHOLD_SIZE = 1024 * 1024 * 1024 - private const val NIO_BUFFER_SIZE = 32 * 1024 * 1024 - private const val NIO_YIELD_SIZE = 128 * 1024 * 1024 - private const val DEFAULT_BUFFER_SIZE = 4 * 1024 * 1024 - private const val DEFAULT_YIELD_SIZE = 16 * 1024 * 1024 - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/prefs/AppPreferences.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/prefs/AppPreferences.kt deleted file mode 100644 index 590f139ba3..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/prefs/AppPreferences.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.arm.aiplayground.data.source.prefs - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Manages internal preferences for the application. - */ -@Singleton -class AppPreferences @Inject constructor ( - @ApplicationContext private val context: Context -) { - companion object { - private const val DATASTORE_APP = "app" - private val Context.appDataStore: DataStore - by preferencesDataStore(name = DATASTORE_APP) - - // Preference keys - private val USER_HAS_IMPORTED_FIRST_MODEL = booleanPreferencesKey("user_has_imported_first_model") - private val USER_HAS_CHATTED_WITH_MODEL = booleanPreferencesKey("user_has_chatted_with_model") - private val USER_HAS_NAVIGATED_TO_MANAGEMENT = booleanPreferencesKey("user_has_navigated_to_management") - } - - /** - * Gets whether the user has imported his first model - */ - fun userHasImportedFirstModel(): Flow = - context.appDataStore.data.map { preferences -> - preferences[USER_HAS_IMPORTED_FIRST_MODEL] == true - } - - /** - * Sets whether the user has completed importing the first model. - */ - suspend fun setUserHasImportedFirstModel(done: Boolean) = withContext(Dispatchers.IO) { - context.appDataStore.edit { preferences -> - preferences[USER_HAS_IMPORTED_FIRST_MODEL] = done - } - } - - /** - * Gets whether the user has chatted with a model - */ - fun userHasChattedWithModel(): Flow = - context.appDataStore.data.map { preferences -> - preferences[USER_HAS_CHATTED_WITH_MODEL] == true - } - - /** - * Sets whether the user has completed chatting with a model. - */ - suspend fun setUserHasChattedWithModel(done: Boolean) = withContext(Dispatchers.IO) { - context.appDataStore.edit { preferences -> - preferences[USER_HAS_CHATTED_WITH_MODEL] = done - } - } - - /** - * Gets whether the user has navigated to model management screen. - */ - fun userHasNavigatedToManagement(): Flow = - context.appDataStore.data.map { preferences -> - preferences[USER_HAS_NAVIGATED_TO_MANAGEMENT] == true - } - - /** - * Sets whether the user has navigated to model management screen. - */ - suspend fun setUserHasNavigatedToManagement(done: Boolean) = withContext(Dispatchers.IO) { - context.appDataStore.edit { preferences -> - preferences[USER_HAS_NAVIGATED_TO_MANAGEMENT] = done - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/prefs/UserPreferences.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/prefs/UserPreferences.kt deleted file mode 100644 index e338c5b5ab..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/prefs/UserPreferences.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.arm.aiplayground.data.source.prefs - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Manages user preferences for the application. - */ -@Singleton -class UserPreferences @Inject constructor ( - @ApplicationContext private val context: Context -) { - - companion object { - private const val DATASTORE_SETTINGS = "settings" - private val Context.settingsDataStore: DataStore - by preferencesDataStore(name = DATASTORE_SETTINGS) - - // Preferences keys - private val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled") - private val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature") - private val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms") - private val COLOR_THEME_MODE = intPreferencesKey("color_theme_mode") - private val DARK_THEME_MODE = intPreferencesKey("dark_theme_mode") - - // Constants - private const val DEFAULT_MONITORING_INTERVAL_MS = 5000L - } - - /** - * Gets whether performance monitoring is enabled. - */ - fun isPerformanceMonitoringEnabled(): Flow = - context.settingsDataStore.data.map { preferences -> - preferences[PERFORMANCE_MONITORING_ENABLED] != false - } - - /** - * Sets whether performance monitoring is enabled. - */ - suspend fun setPerformanceMonitoringEnabled(enabled: Boolean) = withContext(Dispatchers.IO) { - context.settingsDataStore.edit { preferences -> - preferences[PERFORMANCE_MONITORING_ENABLED] = enabled - } - } - - /** - * Gets whether temperature should be displayed in Fahrenheit. - */ - fun usesFahrenheitTemperature(): Flow = - context.settingsDataStore.data.map { preferences -> - preferences[USE_FAHRENHEIT_TEMPERATURE] == true - } - - /** - * Sets whether temperature should be displayed in Fahrenheit. - */ - suspend fun setUseFahrenheitTemperature(useFahrenheit: Boolean) = withContext(Dispatchers.IO) { - context.settingsDataStore.edit { preferences -> - preferences[USE_FAHRENHEIT_TEMPERATURE] = useFahrenheit - } - } - - /** - * Gets the monitoring interval in milliseconds. - * - * TODO-han.yin: replace with Enum value instead of millisecond value - */ - fun getMonitoringInterval(): Flow = - context.settingsDataStore.data.map { preferences -> - preferences[MONITORING_INTERVAL_MS] ?: DEFAULT_MONITORING_INTERVAL_MS - } - - /** - * Sets the monitoring interval in milliseconds. - */ - suspend fun setMonitoringInterval(intervalMs: Long) = withContext(Dispatchers.IO) { - context.settingsDataStore.edit { preferences -> - preferences[MONITORING_INTERVAL_MS] = intervalMs - } - } - - /** - * Gets the current color theme mode. - */ - fun getColorThemeMode(): Flow = - context.settingsDataStore.data.map { preferences -> - ColorThemeMode.fromInt(preferences[COLOR_THEME_MODE]) ?: ColorThemeMode.ARM - } - - /** - * Sets the color theme mode. - */ - suspend fun setColorThemeMode(mode: ColorThemeMode) = withContext(Dispatchers.IO) { - context.settingsDataStore.edit { preferences -> - preferences[COLOR_THEME_MODE] = mode.value - } - } - - /** - * Gets the current dark theme mode. - */ - fun getDarkThemeMode(): Flow = - context.settingsDataStore.data.map { preferences -> - DarkThemeMode.fromInt(preferences[DARK_THEME_MODE]) ?: DarkThemeMode.AUTO - } - - /** - * Sets the dark theme mode. - */ - suspend fun setDarkThemeMode(mode: DarkThemeMode) = withContext(Dispatchers.IO) { - context.settingsDataStore.edit { preferences -> - preferences[DARK_THEME_MODE] = mode.value - } - } -} - -enum class ColorThemeMode(val value: Int, val label: String) { - ARM(0, "Arm®"), - MATERIAL(1, "Material Design"); - - companion object { - fun fromInt(value: Int?) = entries.find { it.value == value } - } -} - -enum class DarkThemeMode(val value: Int, val label: String) { - AUTO(0, "Auto"), - LIGHT(1, "Light"), - DARK(2, "Dark"); - - companion object { - fun fromInt(value: Int?) = entries.find { it.value == value } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/GatedTypeAdapter.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/GatedTypeAdapter.kt deleted file mode 100644 index ad0d7fb0df..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/GatedTypeAdapter.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.arm.aiplayground.data.source.remote - -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import java.lang.reflect.Type - -class GatedTypeAdapter : JsonDeserializer { - override fun deserialize( - json: JsonElement, - typeOfT: Type, - context: JsonDeserializationContext - ): Boolean { - return when { - json.isJsonPrimitive -> { - val primitive = json.asJsonPrimitive - when { - primitive.isBoolean -> primitive.asBoolean - primitive.isString -> primitive.asString != "false" - else -> false - } - } - else -> false - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceApiService.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceApiService.kt deleted file mode 100644 index a8ff426902..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceApiService.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.arm.aiplayground.data.source.remote - -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.HEAD -import retrofit2.http.Path -import retrofit2.http.Query -import retrofit2.http.Streaming - -interface HuggingFaceApiService { - @GET("api/models") - suspend fun getModels( - @Query("search") search: String? = null, - @Query("author") author: String? = null, - @Query("filter") filter: String? = null, - @Query("sort") sort: String? = null, - @Query("direction") direction: String? = null, - @Query("limit") limit: Int? = null, - @Query("full") full: Boolean? = null, - ): List - - @GET("api/models/{modelId}") - suspend fun getModelDetails(@Path("modelId") modelId: String): HuggingFaceModelDetails - - @HEAD("{modelId}/resolve/main/{filename}") - suspend fun getModelFileHeader( - @Path("modelId", encoded = true) modelId: String, - @Path("filename", encoded = true) filename: String - ): Response - - @Deprecated("Use DownloadManager instead!") - @GET("{modelId}/resolve/main/{filename}") - @Streaming - suspend fun downloadModelFile( - @Path("modelId") modelId: String, - @Path("filename") filename: String - ): ResponseBody -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceModel.kt deleted file mode 100644 index d05c97dd3c..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.arm.aiplayground.data.source.remote - -import android.net.Uri -import androidx.core.net.toUri -import com.arm.aiplayground.di.HUGGINGFACE_HOST -import java.util.Date - -internal const val FILE_EXTENSION_GGUF = ".GGUF" -internal val QUANTIZATION_Q4_0 = arrayOf("Q4_0", "Q4-0") - -data class HuggingFaceModel( - val _id: String, - val id: String, - val modelId: String, - - val author: String, - val createdAt: Date, - val lastModified: Date, - - val pipeline_tag: String, - val tags: List, - - val private: Boolean, - val gated: Boolean, - - val likes: Int, - val downloads: Int, - - val sha: String, - val siblings: List, - - val library_name: String?, -) { - fun getGgufFilename(keywords: Array = QUANTIZATION_Q4_0): String? = - siblings.map { it.rfilename } - .filter { - it.endsWith(FILE_EXTENSION_GGUF, ignoreCase = true) } - .firstOrNull { filename -> - keywords.any { filename.contains(it, ignoreCase = true) } - } - - fun anyFilenameContains(keywords: Array): Boolean = - siblings.map { it.rfilename } - .any { filename -> - keywords.any { filename.contains(it, ignoreCase = true) } - } - - fun toDownloadInfo() = getGgufFilename()?.let { - HuggingFaceDownloadInfo(_id, modelId, it) - } -} - -data class HuggingFaceDownloadInfo( - val _id: String, - val modelId: String, - val filename: String, -) { - val uri: Uri - get() = "$HUGGINGFACE_HOST${modelId}/resolve/main/$filename".toUri() -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceModelDetails.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceModelDetails.kt deleted file mode 100644 index b6d8f010c2..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceModelDetails.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.arm.aiplayground.data.source.remote - -import java.util.Date -import kotlin.String - -data class HuggingFaceModelDetails( - val _id: String, - val id: String, - val modelId: String, - - val author: String, - val createdAt: Date, - val lastModified: Date, - - val pipeline_tag: String, - val tags: List, - - val private: Boolean, - val gated: Boolean, - val disabled: Boolean?, - - val likes: Int, - val downloads: Int, - - val usedStorage: Long?, - - val sha: String, - val siblings: List, - val cardData: CardData?, - val widgetData: List?, - - val gguf: Gguf?, - - val library_name: String?, -) { - fun getGgufFilename(keywords: Array = QUANTIZATION_Q4_0): String? = - siblings.map { it.rfilename } - .filter { - it.endsWith(FILE_EXTENSION_GGUF, ignoreCase = true) } - .firstOrNull { filename -> - keywords.any { filename.contains(it, ignoreCase = true) } - } - - fun toModel() = HuggingFaceModel( - _id = this._id, - id = this.id, - modelId = this.modelId, - author = this.author, - createdAt = this.createdAt, - lastModified = this.lastModified, - pipeline_tag = this.pipeline_tag, - tags = this.tags, - private = this.private, - gated = this.gated, - likes = this.likes, - downloads = this.downloads, - sha = this.sha, - siblings = this.siblings.map { Sibling(it.rfilename) }, - library_name = this.library_name, - ) - - fun toDownloadInfo() = getGgufFilename()?.let { - HuggingFaceDownloadInfo(_id, modelId, it) - } -} - -data class Sibling( - val rfilename: String, -) - -data class Gguf( - val total: Long?, - val architecture: String?, - val context_length: Int?, - val chat_template: String?, - val bos_token: String?, - val eos_token: String?, -) - -data class CardData( - val base_model: String?, - val language: List?, - val license: String?, - val pipeline_tag: String?, - val tags: List?, -) - -data class WidgetData( - val text: String -) diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceRemoteDataSource.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceRemoteDataSource.kt deleted file mode 100644 index 4bc648896d..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceRemoteDataSource.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.arm.aiplayground.data.source.remote - -import android.content.Context -import com.arm.aiplayground.monitoring.MemoryMetrics - - -/* - * HuggingFace Search API - */ -private const val QUERY_Q4_0_GGUF = "gguf q4_0" -private const val FILTER_TEXT_GENERATION = "text-generation" -private const val SORT_BY_DOWNLOADS = "downloads" -private const val SEARCH_RESULT_LIMIT = 30 - -private val INVALID_KEYWORDS = arrayOf("-of-", "split", "70B", "30B", "27B", "14B", "13B", "12B") - -interface HuggingFaceRemoteDataSource { - - suspend fun fetchPreselectedModels( - memoryUsage: MemoryMetrics, - parallelCount: Int = 3, - quorum: Float = 0.5f, - ): List - - /** - * Query openly available Q4_0 GGUF models on HuggingFace - */ - suspend fun searchModels( - query: String? = QUERY_Q4_0_GGUF, - filter: String? = FILTER_TEXT_GENERATION, - sort: String? = SORT_BY_DOWNLOADS, - direction: String? = "-1", - limit: Int? = SEARCH_RESULT_LIMIT, - full: Boolean = true, - invalidKeywords: Array = INVALID_KEYWORDS - ): Result> - - suspend fun getModelDetails(modelId: String): HuggingFaceModelDetails - - /** - * Obtain selected HuggingFace model's GGUF file size from HTTP header - */ - suspend fun getFileSize(modelId: String, filePath: String): Result - - /** - * Download selected HuggingFace model's GGUF file via DownloadManager - */ - suspend fun downloadModelFile( - context: Context, - downloadInfo: HuggingFaceDownloadInfo, - ): Result -} - diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceRemoteDataSourceImpl.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceRemoteDataSourceImpl.kt deleted file mode 100644 index 60f22e9ff9..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/data/source/remote/HuggingFaceRemoteDataSourceImpl.kt +++ /dev/null @@ -1,272 +0,0 @@ -package com.arm.aiplayground.data.source.remote - -import android.app.DownloadManager -import android.content.Context -import android.os.Environment -import android.util.Log -import com.arm.aiplayground.monitoring.MemoryMetrics -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.delay -import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import kotlinx.coroutines.withContext -import retrofit2.HttpException -import java.io.FileNotFoundException -import java.io.IOException -import java.net.SocketTimeoutException -import java.net.UnknownHostException -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.collections.contains -import kotlin.coroutines.cancellation.CancellationException -import kotlin.math.ceil - - -/* - * Preselected models: sized <2GB - */ -private val PRESELECTED_MODEL_IDS_SMALL = listOf( - "bartowski/Llama-3.2-1B-Instruct-GGUF", - "unsloth/gemma-3-1b-it-GGUF", - "bartowski/granite-3.0-2b-instruct-GGUF", -) - -/* - * Preselected models: sized 2~3GB - */ -private val PRESELECTED_MODEL_IDS_MEDIUM = listOf( - "bartowski/Llama-3.2-3B-Instruct-GGUF", - "unsloth/gemma-3n-E2B-it-GGUF", - "Qwen/Qwen2.5-3B-Instruct-GGUF", - "gaianet/Phi-4-mini-instruct-GGUF", - "unsloth/gemma-3-4b-it-GGUF", -) - -/* - * Preselected models: sized 4~6B - */ -private val PRESELECTED_MODEL_IDS_LARGE = listOf( - "unsloth/gemma-3n-E4B-it-GGUF", - "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF", -) - -@Singleton -class HuggingFaceRemoteDataSourceImpl @Inject constructor( - private val apiService: HuggingFaceApiService -) : HuggingFaceRemoteDataSource { - - override suspend fun fetchPreselectedModels( - memoryUsage: MemoryMetrics, - parallelCount: Int, - quorum: Float, - ): List = withContext(Dispatchers.IO) { - val ids: List = when { - memoryUsage.availableGB >= 7f -> - PRESELECTED_MODEL_IDS_MEDIUM + PRESELECTED_MODEL_IDS_LARGE + PRESELECTED_MODEL_IDS_SMALL - memoryUsage.availableGB >= 5f -> - PRESELECTED_MODEL_IDS_SMALL + PRESELECTED_MODEL_IDS_MEDIUM + PRESELECTED_MODEL_IDS_LARGE - memoryUsage.availableGB >= 3f -> - PRESELECTED_MODEL_IDS_SMALL + PRESELECTED_MODEL_IDS_MEDIUM - else -> - PRESELECTED_MODEL_IDS_SMALL - } - - val sem = Semaphore(parallelCount) - val results = supervisorScope { - ids.map { id -> - async { - sem.withPermit { - try { - Result.success(getModelDetails(id)) - } catch (t: CancellationException) { - Result.failure(t) - } - } - } - }.awaitAll() - } - - val successes = results.mapNotNull { it.getOrNull() } - val failures = results.mapNotNull { it.exceptionOrNull() } - - val total = ids.size - val failed = failures.size - val ok = successes.size - val shouldThrow = failed >= ceil(total * quorum).toInt() - - if (!shouldThrow) return@withContext successes.toList() - - // 1. No Network - if (failures.count { it is UnknownHostException } >= ceil(failed * 0.5).toInt()) { - throw UnknownHostException() - } - - // 2. Time out - if (failures.count { it is SocketTimeoutException } >= ceil(failed * 0.5).toInt()) { - throw SocketTimeoutException() - } - - // 3. known error codes: 404/410/204 - val http404ish = failures.count { (it as? HttpException)?.code() in listOf(404, 410, 204) } - if (ok == 0 && (failed > 0) && (http404ish >= ceil(failed * 0.5).toInt() || failed == total)) { - throw FileNotFoundException() - } - - // 4. Unknown issues - val ioMajority = failures.count { - it is IOException && it !is UnknownHostException && it !is SocketTimeoutException - } >= ceil(failed * 0.5).toInt() - if (ioMajority) { - throw IOException(failures.first { it is IOException }.message) - } - - successes - } - - override suspend fun searchModels( - query: String?, - filter: String?, - sort: String?, - direction: String?, - limit: Int?, - full: Boolean, - invalidKeywords: Array, - ) = withContext(Dispatchers.IO) { - try { - apiService.getModels( - search = query, - filter = filter, - sort = sort, - direction = direction, - limit = limit, - full = full, - ) - .filterNot { it.gated || it.private } - .filterNot { - it.getGgufFilename().let { filename -> - filename.isNullOrBlank() || invalidKeywords.any { - filename.contains(it, ignoreCase = true) - } - } - }.let { - if (it.isEmpty()) Result.failure(FileNotFoundException()) - else Result.success(it) - } - } catch (e: Exception) { - Log.e(TAG, "Error searching for models on HuggingFace: ${e.message}") - Result.failure(e) - } - } - - override suspend fun getModelDetails( - modelId: String - ) = withContext(Dispatchers.IO) { - apiService.getModelDetails(modelId) - } - - override suspend fun getFileSize( - modelId: String, - filePath: String - ): Result = withContext(Dispatchers.IO) { - try { - apiService.getModelFileHeader(modelId, filePath).let { resp -> - if (resp.isSuccessful) { - resp.headers()[HTTP_HEADER_CONTENT_LENGTH]?.toLongOrNull()?.let { - Result.success(it) - } ?: Result.failure(IOException("Content-Length header missing")) - } else { - Result.failure( - when (resp.code()) { - 401 -> SecurityException("Model requires authentication") - 404 -> FileNotFoundException("Model file not found") - else -> IOException("Failed to get file info: HTTP ${resp.code()}") - } - ) - } - } - } catch (e: Exception) { - Log.e(TAG, "Error getting file size for $modelId: ${e.message}") - Result.failure(e) - } - } - - override suspend fun downloadModelFile( - context: Context, - downloadInfo: HuggingFaceDownloadInfo, - ): Result = withContext(Dispatchers.IO) { - try { - val downloadManager = - context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val request = DownloadManager.Request(downloadInfo.uri).apply { - setTitle(downloadInfo.filename) - setDescription("Downloading directly from HuggingFace") - setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - setDestinationInExternalPublicDir( - Environment.DIRECTORY_DOWNLOADS, - downloadInfo.filename - ) - setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE - ) - setAllowedOverMetered(true) - setAllowedOverRoaming(false) - } - Log.d(TAG, "Enqueuing download request for: ${downloadInfo.modelId}") - val downloadId = downloadManager.enqueue(request) - - delay(DOWNLOAD_MANAGER_DOUBLE_CHECK_DELAY) - - val cursor = downloadManager.query(DownloadManager.Query().setFilterById(downloadId)) - if (cursor != null && cursor.moveToFirst()) { - val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS) - if (statusIndex >= 0) { - val status = cursor.getInt(statusIndex) - cursor.close() - - when (status) { - DownloadManager.STATUS_FAILED -> { - // Get failure reason if available - val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON) - val reason = if (reasonIndex >= 0) cursor.getInt(reasonIndex) else -1 - val errorMessage = when (reason) { - DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP error" - DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient storage" - DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects" - DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code" - DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download" - DownloadManager.ERROR_FILE_ERROR -> "File error" - else -> "Unknown error" - } - Result.failure(Exception(errorMessage)) - } - else -> { - // Download is pending, paused, or running - Result.success(downloadId) - } - } - } else { - // Assume success if we can't check status - cursor.close() - Result.success(downloadId) - } - } else { - // Assume success if cursor is empty - cursor?.close() - Result.success(downloadId) - } - } catch (e: Exception) { - Log.e(TAG, "Failed to enqueue download: ${e.message}") - Result.failure(e) - } - } - - companion object { - private val TAG = HuggingFaceRemoteDataSourceImpl::class.java.simpleName - - private const val HTTP_HEADER_CONTENT_LENGTH = "content-length" - private const val DOWNLOAD_MANAGER_DOUBLE_CHECK_DELAY = 500L - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/di/AppModule.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/di/AppModule.kt deleted file mode 100644 index f1d6551b33..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/di/AppModule.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.arm.aiplayground.di - -import android.content.Context -import com.arm.aichat.AiChat -import com.arm.aichat.InferenceEngine -import com.arm.aichat.TierDetection -import com.arm.aichat.gguf.GgufMetadataReader -import com.arm.aiplayground.data.db.AppDatabase -import com.arm.aiplayground.data.repo.ModelRepository -import com.arm.aiplayground.data.repo.ModelRepositoryImpl -import com.arm.aiplayground.data.repo.SystemPromptRepository -import com.arm.aiplayground.data.repo.SystemPromptRepositoryImpl -import com.arm.aiplayground.data.source.local.LocalFileDataSource -import com.arm.aiplayground.data.source.local.LocalFileDataSourceImpl -import com.arm.aiplayground.data.source.remote.GatedTypeAdapter -import com.arm.aiplayground.data.source.remote.HuggingFaceApiService -import com.arm.aiplayground.data.source.remote.HuggingFaceRemoteDataSource -import com.arm.aiplayground.data.source.remote.HuggingFaceRemoteDataSourceImpl -import com.arm.aiplayground.engine.BenchmarkService -import com.arm.aiplayground.engine.ConversationService -import com.arm.aiplayground.engine.InferenceService -import com.arm.aiplayground.engine.InferenceServiceImpl -import com.arm.aiplayground.engine.ModelLoadingService -import com.arm.aiplayground.engine.StubInferenceEngine -import com.arm.aiplayground.engine.StubTierDetection -import com.arm.aiplayground.monitoring.PerformanceMonitor -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Singleton - -const val HUGGINGFACE_HOST = "https://huggingface.co/" -const val HUGGINGFACE_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - -@Module -@InstallIn(SingletonComponent::class) -internal abstract class AppModule { - - @Binds - abstract fun bindInferenceService(impl: InferenceServiceImpl) : InferenceService - - @Binds - abstract fun bindModelLoadingService(impl: InferenceServiceImpl) : ModelLoadingService - - @Binds - abstract fun bindBenchmarkService(impl: InferenceServiceImpl) : BenchmarkService - - @Binds - abstract fun bindConversationService(impl: InferenceServiceImpl) : ConversationService - - @Binds - abstract fun bindModelsRepository(impl: ModelRepositoryImpl): ModelRepository - - @Binds - abstract fun bindSystemPromptRepository(impl: SystemPromptRepositoryImpl): SystemPromptRepository - - @Binds - abstract fun bindLocalFileDataSource(impl: LocalFileDataSourceImpl) : LocalFileDataSource - - @Binds - abstract fun bindHuggingFaceRemoteDataSource(impl: HuggingFaceRemoteDataSourceImpl): HuggingFaceRemoteDataSource - - companion object { - const val USE_STUB_ENGINE = false - - @Provides - fun provideInferenceEngine(@ApplicationContext context: Context): InferenceEngine { - return if (USE_STUB_ENGINE) { - StubInferenceEngine() - } else { - AiChat.getInferenceEngine(context) - } - } - - @Provides - fun provideTierDetection(@ApplicationContext context: Context): TierDetection { - return if (USE_STUB_ENGINE) { - StubTierDetection - } else { - AiChat.getTierDetection(context) - } - } - - @Provides - fun providePerformanceMonitor(@ApplicationContext context: Context) = PerformanceMonitor(context) - - @Provides - fun provideAppDatabase(@ApplicationContext context: Context) = AppDatabase.getDatabase(context) - - @Provides - fun providesModelDao(appDatabase: AppDatabase) = appDatabase.modelDao() - - @Provides - fun providesSystemPromptDao(appDatabase: AppDatabase) = appDatabase.systemPromptDao() - - @Provides - @Singleton - fun providesGgufMetadataReader(): GgufMetadataReader = GgufMetadataReader.create() - - @Provides - @Singleton - fun provideOkhttpClient() = OkHttpClient.Builder().build() - - @Provides - @Singleton - fun provideGson(): Gson = GsonBuilder() - .setDateFormat(HUGGINGFACE_DATETIME_FORMAT) - .registerTypeAdapter(Boolean::class.java, GatedTypeAdapter()) - .create() - - @Provides - @Singleton - fun provideHuggingFaceApiService( - okHttpClient: OkHttpClient, - gson: Gson, - ): HuggingFaceApiService = - Retrofit.Builder() - .baseUrl(HUGGINGFACE_HOST) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build() - .create(HuggingFaceApiService::class.java) - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/InferenceServices.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/InferenceServices.kt deleted file mode 100644 index 0436a5c454..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/InferenceServices.kt +++ /dev/null @@ -1,290 +0,0 @@ -package com.arm.aiplayground.engine - -import android.util.Log -import com.arm.aichat.InferenceEngine -import com.arm.aichat.InferenceEngine.State -import com.arm.aiplayground.data.model.ModelInfo -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flow -import javax.inject.Inject -import javax.inject.Singleton - -interface InferenceService { - /** - * Expose engine state - */ - val engineState: StateFlow - - /** - * Currently selected model - */ - val currentSelectedModel: StateFlow - - /** - * Set current model - */ - fun setCurrentModel(model: ModelInfo) - - /** - * Unload current model and free resources - */ - suspend fun unloadModel() -} - -interface ModelLoadingService : InferenceService { - /** - * Load a model for benchmark - */ - suspend fun loadModelForBenchmark(): ModelLoadingMetrics? - - /** - * Load a model for conversation - */ - suspend fun loadModelForConversation(systemPrompt: String?): ModelLoadingMetrics? -} - -interface BenchmarkService : InferenceService { - /** - * Run benchmark - * - * @param pp: Prompt Processing size - * @param tg: Token Generation size - * @param pl: Parallel sequences - * @param nr: Number of Runs, i.e. repetitions - */ - suspend fun benchmark(pp: Int, tg: Int, pl: Int, nr: Int): String - - /** - * Benchmark results - */ - val benchmarkResults: StateFlow -} - -interface ConversationService : InferenceService { - /** - * System prompt - */ - val systemPrompt: StateFlow - - /** - * Generate response from prompt - */ - fun generateResponse(prompt: String): Flow - - /** - * Create token metrics based on current state - */ - fun createTokenMetrics(): TokenMetrics -} - -/** - * Metrics for model loading and system prompt processing - */ -data class ModelLoadingMetrics( - val modelLoadingTimeMs: Long, - val systemPromptProcessingTimeMs: Long? = null -) { - val totalTimeMs: Long - get() = modelLoadingTimeMs + (systemPromptProcessingTimeMs ?: 0) -} - -/** - * Represents an update during text generation - */ -data class GenerationUpdate( - val text: String, - val isComplete: Boolean, - val metrics: TokenMetrics? = null -) - -/** - * Metrics for token generation performance - */ -data class TokenMetrics( - val tokensCount: Int, - val ttftMs: Long, - val tpsMs: Float, - val duration: Long, -) { - val text: String - get() = "Tokens: $tokensCount, TTFT: ${ttftMs}ms, TPS: ${"%.1f".format(tpsMs)}" -} - -/** - * Internal implementation of the above [InferenceService]s - */ -@Singleton -internal class InferenceServiceImpl @Inject internal constructor( - private val inferenceEngine: InferenceEngine -) : ModelLoadingService, BenchmarkService, ConversationService { - - /* - * - * InferenceService implementation - * - */ - - override val engineState: StateFlow = inferenceEngine.state - - private val _currentModel = MutableStateFlow(null) - override val currentSelectedModel: StateFlow = _currentModel.asStateFlow() - - override fun setCurrentModel(model: ModelInfo) { _currentModel.value = model } - - override suspend fun unloadModel() = inferenceEngine.cleanUp() - - /** - * Shut down inference engine - */ - fun destroy() = inferenceEngine.destroy() - - /* - * - * ModelLoadingService implementation - * - */ - - override suspend fun loadModelForBenchmark(): ModelLoadingMetrics? { - checkNotNull(_currentModel.value) { "Attempt to load model for bench while none selected!" } - - return _currentModel.value?.let { model -> - try { - val modelLoadStartTs = System.currentTimeMillis() - inferenceEngine.loadModel(model.path) - val modelLoadEndTs = System.currentTimeMillis() - ModelLoadingMetrics(modelLoadEndTs - modelLoadStartTs) - } catch (e: Exception) { - Log.e(TAG, "Error loading model", e) - null - } - } - } - - override suspend fun loadModelForConversation(systemPrompt: String?): ModelLoadingMetrics? { - checkNotNull(_currentModel.value) { "Attempt to load model for chat while none selected!" } - - return _currentModel.value?.let { model -> - try { - _systemPrompt.value = systemPrompt - - val modelLoadStartTs = System.currentTimeMillis() - inferenceEngine.loadModel(model.path) - val modelLoadEndTs = System.currentTimeMillis() - - if (systemPrompt.isNullOrBlank()) { - ModelLoadingMetrics(modelLoadEndTs - modelLoadStartTs) - } else { - val prompt: String = systemPrompt - val systemPromptStartTs = System.currentTimeMillis() - inferenceEngine.setSystemPrompt(prompt) - val systemPromptEndTs = System.currentTimeMillis() - ModelLoadingMetrics( - modelLoadingTimeMs = modelLoadEndTs - modelLoadStartTs, - systemPromptProcessingTimeMs = systemPromptEndTs - systemPromptStartTs - ) - } - } catch (e: Exception) { - Log.e(TAG, e.message, e) - null - } - } - } - - /* - * - * BenchmarkService implementation - * - */ - - override suspend fun benchmark(pp: Int, tg: Int, pl: Int, nr: Int): String = - inferenceEngine.bench(pp, tg, pl, nr).also { - _benchmarkResults.value = it - } - - /** - * Benchmark results if available - */ - private val _benchmarkResults = MutableStateFlow(null) - override val benchmarkResults: StateFlow = _benchmarkResults - - - /* ConversationService implementation */ - - private val _systemPrompt = MutableStateFlow(null) - override val systemPrompt: StateFlow = _systemPrompt.asStateFlow() - - // Token metrics tracking - private var generationStartTime: Long = 0L - private var firstTokenTime: Long = 0L - private var tokenCount: Int = 0 - private var isFirstToken: Boolean = true - - override fun generateResponse(prompt: String): Flow = flow { - val response = StringBuilder() - - try { - // Reset metrics tracking - generationStartTime = System.currentTimeMillis() - firstTokenTime = 0L - tokenCount = 0 - isFirstToken = true - - inferenceEngine.sendUserPrompt(prompt) - .collect { token -> - // Track first token time - if (isFirstToken && token.isNotBlank()) { - firstTokenTime = System.currentTimeMillis() - isFirstToken = false - } - - // Count tokens - if (token.isNotBlank()) { - tokenCount++ - } - - response.append(token) - - // Emit ongoing response (not completed) - emit(GenerationUpdate(response.toString(), false)) - } - - // Calculate final metrics after completion - val metrics = createTokenMetrics() - - // Emit final response with completion flag - emit(GenerationUpdate(response.toString(), true, metrics)) - } catch (e: Exception) { - // Emit error - val metrics = createTokenMetrics() - emit(GenerationUpdate(response.toString(), true, metrics)) - throw e - } - } - - override fun createTokenMetrics(): TokenMetrics { - val endTime = System.currentTimeMillis() - val totalTimeMs = endTime - generationStartTime - - return TokenMetrics( - tokensCount = tokenCount, - ttftMs = if (firstTokenTime > 0) firstTokenTime - generationStartTime else 0L, - tpsMs = calculateTPS(tokenCount, totalTimeMs), - duration = totalTimeMs, - ) - } - - /** - * Calculate tokens per second - */ - private fun calculateTPS(tokens: Int, timeMs: Long): Float { - if (tokens <= 0 || timeMs <= 0) return 0f - return (tokens.toFloat() * 1000f) / timeMs - } - - companion object { - private val TAG = InferenceServiceImpl::class.java.simpleName - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/StubInferenceEngine.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/StubInferenceEngine.kt deleted file mode 100644 index 6d163015f5..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/StubInferenceEngine.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.arm.aiplayground.engine - -import android.util.Log -import com.arm.aichat.InferenceEngine -import com.arm.aichat.InferenceEngine.State -import com.arm.aiplayground.APP_NAME -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Singleton - -/** - * A stub [InferenceEngine] for agile development & testing - */ -@Singleton -class StubInferenceEngine : InferenceEngine { - companion object { - private val TAG = StubInferenceEngine::class.java.simpleName - - private const val STUB_LIBRARY_LOADING_TIME = 2_000L - private const val STUB_MODEL_LOADING_TIME = 3_000L - private const val STUB_MODEL_UNLOADING_TIME = 2_000L - private const val STUB_BENCHMARKING_TIME = 8_000L - private const val STUB_SYSTEM_PROMPT_PROCESSING_TIME = 4_000L - private const val STUB_USER_PROMPT_PROCESSING_TIME = 2_000L - private const val STUB_TOKEN_GENERATION_TIME = 200L - } - - private val _state = MutableStateFlow(State.Uninitialized) - override val state: StateFlow = _state - - private var _readyForSystemPrompt = false - - private val llamaDispatcher = Dispatchers.IO.limitedParallelism(1) - private val llamaScope = CoroutineScope(llamaDispatcher + SupervisorJob()) - - init { - llamaScope.launch { - Log.i(TAG, "Loading and initializing native library!") - _state.value = State.Initializing - - // Simulate library loading - delay(STUB_LIBRARY_LOADING_TIME) - - Log.i(TAG, "Native library initialized!") - _state.value = State.Initialized - } - } - - /** - * Loads a model from the given path. - */ - override suspend fun loadModel(pathToModel: String) = - withContext(llamaDispatcher) { - Log.i(TAG, "loadModel! state: ${_state.value.javaClass.simpleName}") - check(_state.value is State.Initialized) { - "Cannot load model at ${_state.value.javaClass.simpleName}" - } - - try { - _readyForSystemPrompt = false - _state.value = State.LoadingModel - - // Simulate model loading - delay(STUB_MODEL_LOADING_TIME) - - _readyForSystemPrompt = true - _state.value = State.ModelReady - - } catch (e: CancellationException) { - // If coroutine is cancelled, propagate cancellation - throw e - } catch (e: Exception) { - _state.value = State.Error(e) - } - } - - /** - * Process the plain text system prompt - */ - override suspend fun setSystemPrompt(prompt: String) = - withContext(llamaDispatcher) { - check(_state.value is State.ModelReady) { - "Cannot load model at ${_state.value.javaClass.simpleName}" - } - check(_readyForSystemPrompt) { - "System prompt must be set ** RIGHT AFTER ** model loaded!" - } - - try { - _state.value = State.ProcessingSystemPrompt - - // Simulate processing system prompt - delay(STUB_SYSTEM_PROMPT_PROCESSING_TIME) - - _state.value = State.ModelReady - } catch (e: CancellationException) { - // If coroutine is cancelled, propagate cancellation - throw e - } catch (e: Exception) { - _state.value = State.Error(e) - } - } - - /** - * Sends a user prompt to the loaded model and returns a Flow of generated tokens. - */ - override fun sendUserPrompt(message: String, predictLength: Int): Flow = flow { - require(message.isNotEmpty()) { "User prompt discarded due to being empty!" } - check(_state.value is State.ModelReady) { - "Cannot load model at ${_state.value.javaClass.simpleName}" - } - - try { - Log.i(TAG, "sendUserPrompt! \n$message") - _state.value = State.ProcessingUserPrompt - - // Simulate longer processing time - delay(STUB_USER_PROMPT_PROCESSING_TIME) - - _state.value = State.Generating - - // Simulate token generation - val response = "This is a simulated response from the LLM model. The actual implementation would generate tokens one by one based on the input: $message" - response.split(" ").forEach { - emit("$it ") - delay(STUB_TOKEN_GENERATION_TIME) - } - - _state.value = State.ModelReady - } catch (e: CancellationException) { - // Handle cancellation gracefully - _state.value = State.ModelReady - throw e - } catch (e: Exception) { - _state.value = State.Error(e) - throw e - } - }.catch { e -> - // If it's not a cancellation, update state to error - if (e !is CancellationException) { - _state.value = State.Error(Exception(e)) - } - throw e - } - - /** - * Runs a benchmark with the specified parameters. - */ - override suspend fun bench(pp: Int, tg: Int, pl: Int, nr: Int): String = - withContext(llamaDispatcher) { - check(_state.value is State.ModelReady) { - "Cannot load model at ${_state.value.javaClass.simpleName}" - } - - try { - Log.i(TAG, "bench! state: ${_state.value}") - _state.value = State.Benchmarking - - // Simulate benchmark running - delay(STUB_BENCHMARKING_TIME) - - // Generate fake benchmark results - val modelDesc = APP_NAME - val model_size = "7" - val model_n_params = "7" - val backend = "CPU" - - // Random values for benchmarks - val pp_avg = (51.4 + Math.random() * 5.14).toFloat() - val pp_std = (5.14 + Math.random() * 0.514).toFloat() - val tg_avg = (11.4 + Math.random() * 1.14).toFloat() - val tg_std = (1.14 + Math.random() * 0.114).toFloat() - - val result = StringBuilder() - result.append("| model | size | params | backend | test | t/s |\n") - result.append("| --- | --- | --- | --- | --- | --- |\n") - result.append("| $modelDesc | ${model_size}GiB | ${model_n_params}B | ") - result.append("$backend | pp $pp | $pp_avg ± $pp_std |\n") - result.append("| $modelDesc | ${model_size}GiB | ${model_n_params}B | ") - result.append("$backend | tg $tg | $tg_avg ± $tg_std |\n") - - _state.value = State.ModelReady - - result.toString() - } catch (e: CancellationException) { - // If coroutine is cancelled, propagate cancellation - Log.w(TAG, "Unexpected user cancellation while benchmarking!") - _state.value = State.ModelReady - throw e - } catch (e: Exception) { - _state.value = State.Error(e) - "Error: ${e.message}" - } - } - - /** - * Unloads the currently loaded model. - */ - override suspend fun cleanUp() = - withContext(llamaDispatcher) { - when(val state = _state.value) { - is State.ModelReady, is State.Error -> { - Log.i(TAG, "unloadModel! state: ${_state.value.javaClass.simpleName}") - _state.value = State.UnloadingModel - - // Simulate model unloading time - delay(STUB_MODEL_UNLOADING_TIME) - - _state.value = State.Initialized - } - else -> throw IllegalStateException( - "Cannot load model at ${_state.value.javaClass.simpleName}" - ) - } - } - - /** - * Cleans up resources when the engine is no longer needed. - */ - override fun destroy() { - Log.i(TAG, "destroy! state: ${_state.value}") - - _state.value = State.Uninitialized - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/StubTierDetection.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/StubTierDetection.kt deleted file mode 100644 index 4d9955a531..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/engine/StubTierDetection.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.arm.aiplayground.engine - -import com.arm.aichat.ArmCpuTier -import com.arm.aichat.TierDetection -import android.util.Log - -/** - * A stub [TierDetection] for agile development & testing - */ -object StubTierDetection : TierDetection { - private val tag = StubTierDetection::class.java.simpleName - - override fun getDetectedTier(): ArmCpuTier? = ArmCpuTier.T3 - - override fun clearCache() { - Log.d(tag, "Cache cleared") - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/monitoring/PerformanceMonitor.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/monitoring/PerformanceMonitor.kt deleted file mode 100644 index 3faab9b55a..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/monitoring/PerformanceMonitor.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.arm.aiplayground.monitoring - -import android.app.ActivityManager -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.BatteryManager -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.withContext -import javax.inject.Singleton -import kotlin.math.roundToInt - -/** - * Service that monitors device performance metrics such as memory usage, - * battery level, and temperature. - */ -@Singleton -class PerformanceMonitor(@ApplicationContext private val context: Context) { - - /** - * Provides a flow of memory usage information that updates at the specified interval. - */ - fun monitorMemoryUsage(intervalMs: Long = MEMORY_POLLING_INTERVAL): Flow = flow { - while(true) { - emit(getMemoryInfo()) - delay(intervalMs) - } - } - - /** - * Provides a flow of battery information that updates at the specified interval. - */ - fun monitorBattery(intervalMs: Long = BATTERY_POLLING_INTERVAL): Flow = flow { - while(true) { - emit(getBatteryInfo()) - delay(intervalMs) - } - } - - /** - * Provides a flow of temperature information that updates at the specified interval. - */ - fun monitorTemperature(intervalMs: Long = TEMP_POLLING_INTERVAL): Flow = flow { - while(true) { - emit(getTemperatureInfo()) - delay(intervalMs) - } - } - - /** - * Gets the current memory usage information. - */ - private suspend fun getMemoryInfo(): MemoryMetrics = withContext(Dispatchers.IO) { - val mi = ActivityManager.MemoryInfo() - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - activityManager.getMemoryInfo(mi) - - val availableMem = mi.availMem - val totalMem = mi.totalMem - val percentUsed = ((totalMem - availableMem) / totalMem.toFloat() * 100).roundToInt() - - // Convert to more readable units (GB) - val availableGb = (availableMem / (1024.0 * 1024.0 * 1024.0)).toFloat().round(1) - val totalGb = (totalMem / (1024.0 * 1024.0 * 1024.0)).toFloat().round(1) - - MemoryMetrics( - availableMem = availableMem, - totalMem = totalMem, - percentUsed = percentUsed, - availableGB = availableGb, - totalGB = totalGb - ) - } - - /** - * Gets the current battery information. - */ - private fun getBatteryInfo(): BatteryMetrics { - val intent = context.registerReceiver(null, - IntentFilter(Intent.ACTION_BATTERY_CHANGED)) - - val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) ?: 0 - val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, 100) ?: 100 - val batteryPct = level * 100 / scale - - val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 - val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || - status == BatteryManager.BATTERY_STATUS_FULL - - return BatteryMetrics( - level = batteryPct, - isCharging = isCharging - ) - } - - /** - * Gets the current temperature information. - */ - private fun getTemperatureInfo(): TemperatureMetrics { - val intent = context.registerReceiver(null, - IntentFilter(Intent.ACTION_BATTERY_CHANGED)) - - // Battery temperature is reported in tenths of a degree Celsius - val tempTenthsC = intent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0 - val tempC = tempTenthsC / 10.0f - - val warningLevel = when { - tempC >= 40.0f -> TemperatureWarningLevel.HIGH - tempC >= 35.0f -> TemperatureWarningLevel.MEDIUM - else -> TemperatureWarningLevel.NORMAL - } - - return TemperatureMetrics( - tempCelsiusValue = tempC, - warningLevel = warningLevel - ) - } - - private fun Float.round(decimals: Int): Float { - var multiplier = 1.0f - repeat(decimals) { multiplier *= 10 } - return (this * multiplier).roundToInt() / multiplier - } - - companion object { - private const val MEMORY_POLLING_INTERVAL = 5000L - private const val BATTERY_POLLING_INTERVAL = 10000L - private const val TEMP_POLLING_INTERVAL = 10000L - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/monitoring/SystemMetrics.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/monitoring/SystemMetrics.kt deleted file mode 100644 index affaae53de..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/monitoring/SystemMetrics.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.arm.aiplayground.monitoring - -/** - * Data class containing battery information. - */ -data class BatteryMetrics( - val level: Int, - val isCharging: Boolean -) - -/** - * Data class containing memory usage metrics. - */ -data class MemoryMetrics( - val availableMem: Long, - val totalMem: Long, - val percentUsed: Int, - val availableGB: Float, - val totalGB: Float -) - -/** - * Data class containing temperature information. - */ -data class TemperatureMetrics( - private val tempCelsiusValue: Float, - val warningLevel: TemperatureWarningLevel -) { - val celsiusDisplay: String - get() = "${tempCelsiusValue.toInt()}°C" - - val fahrenheitDisplay: String - get() = "${(tempCelsiusValue * 9/5 + 32).toInt()}°F" - - fun getDisplay(useFahrenheit: Boolean) = - if (useFahrenheit) fahrenheitDisplay else celsiusDisplay -} - -enum class TemperatureWarningLevel { - NORMAL, - MEDIUM, - HIGH -} - -/** - * Data class containing storage usage metrics. - */ -data class StorageMetrics( - val usedGB: Float, - val availableGB: Float -) diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt deleted file mode 100644 index f4641dc6f6..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/navigation/AppDestinations.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.arm.aiplayground.navigation - -import androidx.navigation.NavController -import com.arm.aiplayground.engine.ModelLoadingMetrics - -/** - * Navigation destinations for the app - */ -object AppDestinations { - // Primary navigation destinations - const val MODELS_ROUTE = "models" - const val MODEL_LOADING_ROUTE = "model_loading" - - const val CONVERSATION_ROUTE = "conversation" - const val CONVERSATION_ROUTE_WITH_PARAMS = "conversation/{modelLoadTimeMs}/{promptProcessTimeMs}" - const val CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME = "modelLoadTimeMs" - const val CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME = "promptProcessTimeMs" - - const val BENCHMARK_ROUTE = "benchmark" - const val BENCHMARK_ROUTE_WITH_PARAMS = "benchmark/{modelLoadTimeMs}" - const val BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME = "modelLoadTimeMs" - - // Settings destinations - const val SETTINGS_GENERAL_ROUTE = "settings_general" -} - -/** - * Navigation actions to be performed in the app - */ -class NavigationActions(private val navController: NavController) { - - fun navigateToModelSelection() { - navController.navigate(AppDestinations.MODELS_ROUTE) { - // Clear back stack to start fresh - popUpTo(AppDestinations.MODELS_ROUTE) { inclusive = true } - } - } - - fun navigateToModelLoading() { - navController.navigate(AppDestinations.MODEL_LOADING_ROUTE) - } - - fun navigateToConversation(metrics: ModelLoadingMetrics) { - val route = AppDestinations.CONVERSATION_ROUTE - val modelLoadTimeMs = metrics.modelLoadingTimeMs - val promptTimeMs = metrics.systemPromptProcessingTimeMs ?: 0 - navController.navigate("$route/$modelLoadTimeMs/$promptTimeMs") - } - - fun navigateToBenchmark(metrics: ModelLoadingMetrics) { - val route = AppDestinations.BENCHMARK_ROUTE - val modelLoadTimeMs = metrics.modelLoadingTimeMs - navController.navigate("$route/$modelLoadTimeMs") - } - - fun navigateToSettingsGeneral() { - navController.navigate(AppDestinations.SETTINGS_GENERAL_ROUTE) - } - - fun navigateUp() { - navController.navigateUp() - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/AppContent.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/AppContent.kt deleted file mode 100644 index 5c15cd166c..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/AppContent.kt +++ /dev/null @@ -1,566 +0,0 @@ -package com.arm.aiplayground.ui - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.DrawerValue -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.rememberDrawerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import com.arm.aichat.InferenceEngine -import com.arm.aichat.isUninterruptible -import com.arm.aiplayground.engine.ModelLoadingMetrics -import com.arm.aiplayground.navigation.AppDestinations -import com.arm.aiplayground.navigation.AppDestinations.BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME -import com.arm.aiplayground.navigation.AppDestinations.CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME -import com.arm.aiplayground.navigation.AppDestinations.CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME -import com.arm.aiplayground.navigation.NavigationActions -import com.arm.aiplayground.ui.scaffold.AnimatedNavHost -import com.arm.aiplayground.ui.scaffold.AppNavigationDrawer -import com.arm.aiplayground.ui.scaffold.AppScaffold -import com.arm.aiplayground.ui.scaffold.ScaffoldConfig -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import com.arm.aiplayground.ui.scaffold.bottombar.BottomBarConfig -import com.arm.aiplayground.ui.scaffold.topbar.NavigationIcon -import com.arm.aiplayground.ui.scaffold.topbar.TopBarConfig -import com.arm.aiplayground.ui.screens.BenchmarkScreen -import com.arm.aiplayground.ui.screens.ConversationScreen -import com.arm.aiplayground.ui.screens.ModelLoadingScreen -import com.arm.aiplayground.ui.screens.ModelsScreen -import com.arm.aiplayground.ui.screens.SettingsGeneralScreen -import com.arm.aiplayground.viewmodel.BenchmarkViewModel -import com.arm.aiplayground.viewmodel.ConversationViewModel -import com.arm.aiplayground.viewmodel.MainViewModel -import com.arm.aiplayground.viewmodel.ModelLoadingViewModel -import com.arm.aiplayground.viewmodel.ModelScreenUiMode -import com.arm.aiplayground.viewmodel.ModelsManagementViewModel -import com.arm.aiplayground.viewmodel.ModelsViewModel -import com.arm.aiplayground.viewmodel.SettingsViewModel -import kotlinx.coroutines.launch - -@Composable -fun AppContent( - settingsViewModel: SettingsViewModel, - mainViewModel: MainViewModel = hiltViewModel(), - modelsViewModel: ModelsViewModel = hiltViewModel(), - modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(), - modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(), - benchmarkViewModel: BenchmarkViewModel = hiltViewModel(), - conversationViewModel: ConversationViewModel = hiltViewModel(), -) { - val coroutineScope = rememberCoroutineScope() - val snackbarHostState = remember { SnackbarHostState() } - - // App core states - val engineState by mainViewModel.engineState.collectAsState() - val showModelImportTooltip by mainViewModel.showModelImportTooltip.collectAsState() - val showChatTooltip by mainViewModel.showChatTooltip.collectAsState() - val showManagementTooltip by mainViewModel.showModelManagementTooltip.collectAsState() - - // Model state - val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() - - // Metric states for scaffolds - val isMonitoringEnabled by settingsViewModel.isMonitoringEnabled.collectAsState() - val memoryUsage by settingsViewModel.memoryUsage.collectAsState() - val temperatureInfo by settingsViewModel.temperatureMetrics.collectAsState() - val useFahrenheit by settingsViewModel.useFahrenheitUnit.collectAsState() - val storageMetrics by settingsViewModel.storageMetrics.collectAsState() - - // Navigation - val navController = rememberNavController() - val navigationActions = remember(navController) { NavigationActions(navController) } - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute by remember(navBackStackEntry) { - derivedStateOf { navBackStackEntry?.destination?.route ?: "" } - } - - // Determine if drawer gestures should be enabled based on route - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val drawerGesturesEnabled by remember(currentRoute, drawerState.currentValue) { - derivedStateOf { - // Always allow gesture dismissal when drawer is open - if (drawerState.currentValue == DrawerValue.Open) true else false - } - } - val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } } - - // Handle child screens' scaffold events - val handleScaffoldEvent: (ScaffoldEvent) -> Unit = { event -> - when (event) { - is ScaffoldEvent.ShowSnackbar -> { - coroutineScope.launch { - if (event.actionLabel != null && event.onAction != null) { - val result = snackbarHostState.showSnackbar( - message = event.message, - actionLabel = event.actionLabel, - withDismissAction = event.withDismissAction, - duration = event.duration - ) - if (result == SnackbarResult.ActionPerformed) { - event.onAction() - } - } else { - snackbarHostState.showSnackbar( - message = event.message, - withDismissAction = event.withDismissAction, - duration = event.duration - ) - } - } - } - is ScaffoldEvent.ChangeTitle -> { - // TODO-han.yin: TBD - } - is ScaffoldEvent.ShareText -> { - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, event.text) - event.title?.let { putExtra(Intent.EXTRA_SUBJECT, it) } - type = event.mimeType - } - - val shareChooser = Intent.createChooser(shareIntent, event.title ?: "Share via") - - // Use the current activity for context - val context = (navController.context as? Activity) - ?: throw IllegalStateException("Activity context required for sharing") - - try { - context.startActivity(shareChooser) - } catch (_: ActivityNotFoundException) { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "No app found to share content", - duration = SnackbarDuration.Short - ) - } - } catch (e: Exception) { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "Share failed due to ${e.message}", - duration = SnackbarDuration.Short - ) - } - } - } - } - } - - // Create scaffold's top & bottom bar configs based on current route - val scaffoldConfig = when { - // Model selection screen - currentRoute == AppDestinations.MODELS_ROUTE -> { - // Collect states for bottom bar - val allModels by modelsViewModel.allModels.collectAsState() - val filteredModels by modelsViewModel.filteredModels.collectAsState() - val sortOrder by modelsViewModel.sortOrder.collectAsState() - val showSortMenu by modelsViewModel.showSortMenu.collectAsState() - val activeFilters by modelsViewModel.activeFilters.collectAsState() - val showFilterMenu by modelsViewModel.showFilterMenu.collectAsState() - val preselection by modelsViewModel.preselectedModelToRun.collectAsState() - - val selectedModelsToDelete by modelsManagementViewModel.selectedModelsToDelete.collectAsState() - val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState() - - val hasModelsInstalled = allModels?.isNotEmpty() == true - - // Create file launcher for importing local models - val fileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument() - ) { uri -> uri?.let { modelsManagementViewModel.importLocalModelFileSelected(it) } } - - ScaffoldConfig( - topBarConfig = - when (modelScreenUiMode) { - ModelScreenUiMode.BROWSING -> - TopBarConfig.ModelsBrowsing( - title = "Installed models", - navigationIcon = NavigationIcon.Menu { - modelsViewModel.resetPreselection() - openDrawer() - }, - showTooltip = showManagementTooltip && !showChatTooltip && hasModelsInstalled, - showManagingToggle = !showChatTooltip && hasModelsInstalled, - onToggleManaging = { - if (hasModelsInstalled) { - mainViewModel.waiveModelManagementTooltip() - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - } - }, - ) - - ModelScreenUiMode.SEARCHING -> - TopBarConfig.None() - - ModelScreenUiMode.MANAGING -> - TopBarConfig.ModelsManagement( - title = "Managing models", - navigationIcon = NavigationIcon.Back { - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - }, - storageMetrics = - if (isMonitoringEnabled && !showModelImportTooltip) - storageMetrics else null, - ) - - ModelScreenUiMode.DELETING -> - TopBarConfig.ModelsDeleting( - title = "Deleting models", - navigationIcon = NavigationIcon.Quit { - modelsManagementViewModel.resetManagementState() - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - }, - ) - }, - bottomBarConfig = - when (modelScreenUiMode) { - ModelScreenUiMode.BROWSING -> - BottomBarConfig.Models.Browsing( - isSearchingEnabled = hasModelsInstalled, - onToggleSearching = { - modelsViewModel.toggleMode(ModelScreenUiMode.SEARCHING) - }, - sorting = BottomBarConfig.Models.Browsing.SortingConfig( - isEnabled = hasModelsInstalled, - currentOrder = sortOrder, - isMenuVisible = showSortMenu, - toggleMenu = modelsViewModel::toggleSortMenu, - selectOrder = { - modelsViewModel.setSortOrder(it) - modelsViewModel.toggleSortMenu(false) - } - ), - filtering = BottomBarConfig.Models.Browsing.FilteringConfig( - isEnabled = hasModelsInstalled, - filters = activeFilters, - onToggleFilter = modelsViewModel::toggleFilter, - onClearFilters = modelsViewModel::clearFilters, - isMenuVisible = showFilterMenu, - toggleMenu = modelsViewModel::toggleFilterMenu - ), - runAction = BottomBarConfig.Models.RunActionConfig( - showTooltip = showChatTooltip, - preselectedModelToRun = preselection, - onClickRun = { - if (modelsViewModel.selectModel(it)) { - modelsViewModel.resetPreselection() - navigationActions.navigateToModelLoading() - } - } - ), - ) - - ModelScreenUiMode.SEARCHING -> - BottomBarConfig.Models.Searching( - textFieldState = modelsViewModel.searchFieldState, - onQuitSearching = { - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - }, - onSearch = { /* No-op for now */ }, - runAction = BottomBarConfig.Models.RunActionConfig( - showTooltip = false, - preselectedModelToRun = preselection, - onClickRun = { - if (modelsViewModel.selectModel(it)) { - modelsViewModel.resetPreselection() - navigationActions.navigateToModelLoading() - } - } - ), - ) - - ModelScreenUiMode.MANAGING -> - BottomBarConfig.Models.Managing( - isDeletionEnabled = filteredModels?.isNotEmpty() == true, - onToggleDeleting = { - modelsViewModel.toggleMode(ModelScreenUiMode.DELETING) - }, - sorting = BottomBarConfig.Models.Managing.SortingConfig( - isEnabled = hasModelsInstalled, - currentOrder = sortOrder, - isMenuVisible = showSortMenu, - toggleMenu = { modelsViewModel.toggleSortMenu(it) }, - selectOrder = { - modelsViewModel.setSortOrder(it) - modelsViewModel.toggleSortMenu(false) - } - ), - filtering = BottomBarConfig.Models.Managing.FilteringConfig( - isEnabled = hasModelsInstalled, - filters = activeFilters, - onToggleFilter = modelsViewModel::toggleFilter, - onClearFilters = modelsViewModel::clearFilters, - isMenuVisible = showFilterMenu, - toggleMenu = modelsViewModel::toggleFilterMenu - ), - importing = BottomBarConfig.Models.Managing.ImportConfig( - showTooltip = showModelImportTooltip, - isMenuVisible = showImportModelMenu, - toggleMenu = { show -> - modelsManagementViewModel.toggleImportMenu(show) - }, - importFromLocal = { - fileLauncher.launch(arrayOf("application/octet-stream", "*/*")) - modelsManagementViewModel.toggleImportMenu(false) - }, - importFromHuggingFace = { - modelsManagementViewModel.queryModelsFromHuggingFace(memoryUsage) - modelsManagementViewModel.toggleImportMenu(false) - } - ), - ) - - ModelScreenUiMode.DELETING -> - BottomBarConfig.Models.Deleting( - onQuitDeleting = { - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - }, - selectedModels = selectedModelsToDelete, - selectAllFilteredModels = { - filteredModels?.let { - modelsManagementViewModel.selectModelsToDelete(it) - } - }, - clearAllSelectedModels = { - modelsManagementViewModel.clearSelectedModelsToDelete() - }, - deleteSelected = { - selectedModelsToDelete.let { - if (it.isNotEmpty()) { - modelsManagementViewModel.batchDeletionClicked(it) - } - } - }, - ) - } - ) - } - - // Model loading screen - currentRoute == AppDestinations.MODEL_LOADING_ROUTE -> - ScaffoldConfig( - topBarConfig = TopBarConfig.Performance( - title = "Select a mode", - navigationIcon = NavigationIcon.Back { - modelLoadingViewModel.onBackPressed { navigationActions.navigateUp() } - }, - memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, - temperatureInfo = null - ) - ) - - // Benchmark screen - currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> { - val showShareFab by benchmarkViewModel.showShareFab.collectAsState() - val showModelCard by benchmarkViewModel.showModelCard.collectAsState() - - ScaffoldConfig( - topBarConfig = TopBarConfig.Performance( - title = "Benchmark", - navigationIcon = NavigationIcon.Back { - benchmarkViewModel.onBackPressed { navigationActions.navigateUp() } - }, - memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, - temperatureInfo = if (isMonitoringEnabled) Pair( - temperatureInfo, - useFahrenheit - ) else null - ), - bottomBarConfig = BottomBarConfig.Benchmark( - engineIdle = !engineState.isUninterruptible, - showShareFab = showShareFab, - onShare = { benchmarkViewModel.shareResult(handleScaffoldEvent) }, - onRerun = { benchmarkViewModel.rerunBenchmark(handleScaffoldEvent) }, - onClear = { benchmarkViewModel.clearResults(handleScaffoldEvent) }, - showModelCard = showModelCard, - onToggleModelCard = benchmarkViewModel::toggleModelCard, - ) - ) - } - - // Conversation screen - currentRoute.startsWith(AppDestinations.CONVERSATION_ROUTE) -> { - val showModelCard by conversationViewModel.showModelCard.collectAsState() - - val modelThinkingOrSpeaking = - engineState is InferenceEngine.State.ProcessingUserPrompt || engineState is InferenceEngine.State.Generating - - ScaffoldConfig( - topBarConfig = TopBarConfig.Performance( - title = "Chat", - navigationIcon = NavigationIcon.Back { - conversationViewModel.onBackPressed { navigationActions.navigateUp() } - }, - memoryMetrics = if (isMonitoringEnabled) memoryUsage else null, - temperatureInfo = if (isMonitoringEnabled) Pair( - temperatureInfo, - useFahrenheit - ) else null, - ), - bottomBarConfig = BottomBarConfig.Conversation( - isEnabled = !modelThinkingOrSpeaking, - textFieldState = conversationViewModel.inputFieldState, - onSendClick = conversationViewModel::sendMessage, - showModelCard = showModelCard, - onToggleModelCard = conversationViewModel::toggleModelCard, - ) - ) - } - - // Settings screen - currentRoute == AppDestinations.SETTINGS_GENERAL_ROUTE -> - ScaffoldConfig( - topBarConfig = TopBarConfig.Default( - title = "Settings", - navigationIcon = NavigationIcon.Back { navigationActions.navigateUp() } - ) - ) - - // Fallback for empty screen or unknown routes - else -> ScaffoldConfig( - topBarConfig = TopBarConfig.Default(title = "", navigationIcon = NavigationIcon.None) - ) - } - - // Main UI hierarchy - AppNavigationDrawer( - drawerState = drawerState, - navigationActions = navigationActions, - gesturesEnabled = drawerGesturesEnabled, - currentRoute = currentRoute - ) { - // The AppScaffold now uses the config we created - AppScaffold( - topBarconfig = scaffoldConfig.topBarConfig, - bottomBarConfig = scaffoldConfig.bottomBarConfig, - onScaffoldEvent = handleScaffoldEvent, - snackbarHostState = snackbarHostState, - ) { paddingValues -> - // AnimatedNavHost inside the scaffold content - AnimatedNavHost( - navController = navController, - startDestination = AppDestinations.MODELS_ROUTE, - modifier = Modifier.Companion.padding(paddingValues) - ) { - // Model Selection Screen - composable(AppDestinations.MODELS_ROUTE) { - ModelsScreen( - showModelImportTooltip = showModelImportTooltip, - onFirstModelImportSuccess = { model -> - if (showModelImportTooltip) { - mainViewModel.waiveModelImportTooltip() - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - modelsViewModel.preselectModel(model, true) - } - }, - showChatTooltip = showChatTooltip, - onConfirmSelection = { modelInfo, ramWarning -> - if (modelsViewModel.confirmSelectedModel(modelInfo, ramWarning)) { - navigationActions.navigateToModelLoading() - } - }, - onScaffoldEvent = handleScaffoldEvent, - modelsViewModel = modelsViewModel, - managementViewModel = modelsManagementViewModel, - ) - } - - // Mode Selection Screen - composable(AppDestinations.MODEL_LOADING_ROUTE) { - ModelLoadingScreen( - onScaffoldEvent = handleScaffoldEvent, - onNavigateBack = { navigationActions.navigateUp() }, - onNavigateToBenchmark = { navigationActions.navigateToBenchmark(it) }, - onNavigateToConversation = { - navigationActions.navigateToConversation(it) - if (showChatTooltip) { - mainViewModel.waiveChatTooltip() - } - }, - viewModel = modelLoadingViewModel - ) - } - - // Benchmark Screen - composable( - route = AppDestinations.BENCHMARK_ROUTE_WITH_PARAMS, - arguments = listOf( - navArgument(BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME) { - type = NavType.Companion.LongType - defaultValue = 0L - } - ) - ) { backStackEntry -> - val modelLoadTimeMs = backStackEntry.arguments?.getLong(BENCHMARK_ROUTE_PARAM_MODEL_LOAD_TIME) ?: 0L - val metrics = if (modelLoadTimeMs > 0) { - ModelLoadingMetrics(modelLoadTimeMs) - } else throw IllegalArgumentException("Expecting a valid ModelLoadingMetrics!") - - BenchmarkScreen( - loadingMetrics = metrics, - onScaffoldEvent = handleScaffoldEvent, - onNavigateBack = { navigationActions.navigateUp() }, - viewModel = benchmarkViewModel - ) - } - - // Conversation Screen - composable( - route = AppDestinations.CONVERSATION_ROUTE_WITH_PARAMS, - arguments = listOf( - navArgument(CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME) { - type = NavType.Companion.LongType - defaultValue = 0L - }, - navArgument(CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME) { - type = NavType.Companion.LongType - defaultValue = 0L - } - ) - ) { backStackEntry -> - val modelLoadTimeMs = backStackEntry.arguments?.getLong( - CONVERSATION_ROUTE_PARAM_MODEL_LOAD_TIME) ?: 0L - val promptProcessTimeMs = backStackEntry.arguments?.getLong( - CONVERSATION_ROUTE_PARAM_PROMPT_PROCESS_TIME) ?: 0L - val metrics = if (modelLoadTimeMs > 0) { - ModelLoadingMetrics( - modelLoadingTimeMs = modelLoadTimeMs, - systemPromptProcessingTimeMs = if (promptProcessTimeMs > 0) promptProcessTimeMs else null - ) - } else throw IllegalArgumentException("Expecting a valid ModelLoadingMetrics!") - - ConversationScreen( - loadingMetrics = metrics, - onNavigateBack = { navigationActions.navigateUp() }, - viewModel = conversationViewModel - ) - } - - // Settings Screen - composable(AppDestinations.SETTINGS_GENERAL_ROUTE) { - SettingsGeneralScreen( - viewModel = settingsViewModel - ) - } - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ArmFeaturesVisualizer.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ArmFeaturesVisualizer.kt deleted file mode 100644 index ed3ae6215e..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ArmFeaturesVisualizer.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.arm.aiplayground.ui.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MultiChoiceSegmentedButtonRow -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import com.arm.aichat.ArmFeature -import com.arm.aichat.ArmFeaturesMapper.DisplayItem -import kotlin.math.sqrt - -/** - * ARM Features visualization using segmented buttons. - */ -@Composable -fun ArmFeaturesVisualizer( - supportedFeatures: List, - autoSizeMinScaling: Float = 0.5f, - onFeatureClick: ((ArmFeature) -> Unit)? = null -) { - // Segmented Button Row for Features - MultiChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - supportedFeatures.forEachIndexed { index, item -> - val weight = sqrt(item.feature.displayName.length.toFloat()) - - SegmentedButton( - modifier = Modifier.weight(weight), - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = supportedFeatures.size - ), - icon = {}, - onCheckedChange = { onFeatureClick?.invoke(item.feature) }, - checked = item.isSupported, - ) { - Text( - text = item.feature.displayName, - autoSize = TextAutoSize.StepBased( - minFontSize = MaterialTheme.typography.labelSmall.fontSize * autoSizeMinScaling, - maxFontSize = MaterialTheme.typography.labelSmall.fontSize - ), - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - fontWeight = if (item.isSupported) { - FontWeight.Medium - } else { - FontWeight.Light - }, - color = if (item.isSupported) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - } - ) - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/InfoView.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/InfoView.kt deleted file mode 100644 index 886268b45c..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/InfoView.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.arm.aiplayground.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties - -data class InfoAction( - val icon: ImageVector, - val label: String, - val onAction: () -> Unit -) - -@Composable -fun InfoAlertDialog( - modifier: Modifier = Modifier, - isCritical: Boolean = false, - allowDismiss: Boolean = false, - onDismiss: () -> Unit = {}, - icon: ImageVector, - title: String, - message: String? = null, - action: InfoAction? = null, - confirmButton: @Composable () -> Unit = {}, -) { - AlertDialog( - modifier = modifier, - onDismissRequest = onDismiss, - properties = DialogProperties( - dismissOnBackPress = allowDismiss, - dismissOnClickOutside = allowDismiss, - ), - icon = { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(64.dp), - ) - }, - title = { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold - ) - }, - text = { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - message?.let { - Text( - modifier = Modifier.padding(top = 8.dp), - text = it, - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - action?.let { - Button( - modifier = Modifier.padding(top = 24.dp), - onClick = it.onAction, - ) { - Icon( - imageVector = it.icon, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(it.label) - } - } - } - - }, - containerColor = if (isCritical) MaterialTheme.colorScheme.errorContainer else AlertDialogDefaults.containerColor, - iconContentColor = if (isCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, - titleContentColor = if (isCritical) MaterialTheme.colorScheme.onErrorContainer else AlertDialogDefaults.titleContentColor, - textContentColor = if (isCritical) MaterialTheme.colorScheme.onErrorContainer else AlertDialogDefaults.textContentColor, - confirmButton = confirmButton, - ) -} - -@Composable -fun InfoView( - modifier: Modifier = Modifier, - icon: ImageVector, - title: String, - message: String? = null, - action: InfoAction? = null -) { - InfoView( - modifier = modifier, - title = title, - icon = { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - ) - }, - message = message, - action = action - ) -} - -@Composable -fun InfoView( - modifier: Modifier = Modifier, - icon: @Composable () -> Unit, - title: String, - message: String? = null, - action: InfoAction? = null -) { - Column( - modifier = modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - icon() - - Text( - modifier = Modifier.padding(top = 16.dp), - text = title, - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold - ) - - message?.let { - Text( - modifier = Modifier.padding(top = 8.dp), - text = it, - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - action?.let { - Button( - modifier = Modifier.padding(top = 24.dp), - onClick = it.onAction, - ) { - Icon( - imageVector = it.icon, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(it.label) - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ModelCards.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ModelCards.kt deleted file mode 100644 index f27ff43a3d..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ModelCards.kt +++ /dev/null @@ -1,395 +0,0 @@ -package com.arm.aiplayground.ui.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material3.AssistChip -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Checkbox -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.util.languageCodeToFlagEmoji -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -/** - * Displays model information in a card format expandable with core details - * such as context length, architecture, quantization and file size - * - * @param model The model information to display - * @param isExpanded Whether additional details is expanded or shrunk - * @param onExpanded Action to perform when the card is expanded or shrunk - */ -@Composable -fun ModelCardCoreExpandable( - model: ModelInfo, - containerColor: Color = MaterialTheme.colorScheme.primaryContainer, - isExpanded: Boolean = false, - onExpanded: ((Boolean) -> Unit)? = null -) = ModelCardCoreExpandable( - model = model, - containerColor = containerColor, - isExpanded = isExpanded, - onExpanded = onExpanded -) { - Spacer(modifier = Modifier.height(8.dp)) - - // Row 2: Context length, size label - ModelCardContentContextRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 3: Architecture, quantization, formatted size - ModelCardContentArchitectureRow(model) -} - -/** - * Displays model information in a card format expandable with customizable extra content. - * - * @param model The model information to display - * @param isExpanded Whether additional details is expanded or shrunk - * @param onExpanded Action to perform when the card is expanded or shrunk - */ -@Composable -fun ModelCardCoreExpandable( - model: ModelInfo, - containerColor: Color = MaterialTheme.colorScheme.primaryContainer, - isExpanded: Boolean = false, - onExpanded: ((Boolean) -> Unit)? = null, - expandableSection: @Composable () -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { onExpanded?.invoke(!isExpanded) }, - colors = when (isExpanded) { - true -> CardDefaults.cardColors(containerColor = containerColor) - false -> CardDefaults.cardColors() - }, - elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - ) { - // Row 1: Model full name + chevron - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - ModelCardContentTitleRow(model) - - CompositionLocalProvider( - LocalMinimumInteractiveComponentSize provides Dp.Unspecified - ) { - IconButton(onClick = { onExpanded?.invoke(!isExpanded) }) { - Icon( - imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, - contentDescription = "Tap to ${if (isExpanded) "shrink" else "expand"} model card" - ) - } - } - } - - // Expandable content - AnimatedVisibility( - visible = isExpanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier.weight(1f) - ) { - expandableSection() - } - } - } - } -} - -/** - * Displays model information in a card format with expandable details. - * - * This component shows essential model information and can be expanded to show - * additional details such as dates, tags, and languages. - * The expanded state is toggled by clicking on the content area of the card. - * - * @param model The model information to display - * @param isSelected Optional selection state (shows checkbox when not null) - * @param onSelected Action to perform when the card is selected (in multi-selection mode) - * @param isExpanded Whether additional details is expanded or shrunk - * @param onExpanded Action to perform when the card is expanded or shrunk - */ -@Composable -fun ModelCardFullExpandable( - model: ModelInfo, - containerColor: Color = MaterialTheme.colorScheme.primaryContainer, - isSelected: Boolean? = null, - onSelected: ((Boolean) -> Unit)? = null, - isExpanded: Boolean = false, - onExpanded: ((Boolean) -> Unit)? = null, -) { - CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { onExpanded?.invoke(!isExpanded) }, - colors = when (isSelected) { - true -> CardDefaults.cardColors(containerColor = containerColor) - false -> CardDefaults.cardColors() - else -> CardDefaults.cardColors() - }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column( - modifier = Modifier.padding(bottom = 16.dp) - ) { - Row( - verticalAlignment = Alignment.Top - ) { - // Show checkbox if in selection mode - isSelected?.let { selected -> - Checkbox( - checked = selected, - onCheckedChange = { onSelected?.invoke(it) }, - modifier = Modifier.padding(top = 16.dp, start = 16.dp) - ) - } - - Box( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp, top = 16.dp, end = 16.dp) - ) { - // Row 1: Model full name - ModelCardContentTitleRow(model) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - // Row 2: Context length, size label - ModelCardContentContextRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 3: Architecture, quantization, formatted size - ModelCardContentArchitectureRow(model) - } - - // Expandable content - AnimatedVisibility( - visible = isExpanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp) - ) { - Column(modifier = Modifier.padding(top = 12.dp)) { - // Divider between core and expanded content - HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp)) - - // Row 4: Dates - ModelCardContentDatesRow(model) - - // Row 5: Tags - model.tags?.let { tags -> - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = "Tags", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(6.dp)) - - ModelCardContentTagsSection(tags) - } - - // Row 6: Languages - model.languages?.let { languages -> - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = "Supported languages", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(6.dp)) - - ModelCardContentLanguagesSections(languages) - } - } - } - } - } - } - } -} - -/* - * - * Individual components to be orchestrated into a model card's content - * - */ - -@Composable -fun ModelCardContentTitleRow(model: ModelInfo) = - Text( - text = model.formattedFullName, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium - ) - -@Composable -fun ModelCardContentContextRow(model: ModelInfo) = - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - ModelCardContentField("Context", model.formattedContextLength) - - ModelCardContentField("Params", model.formattedParamSize) - } - -@Composable -fun ModelCardContentArchitectureRow(model: ModelInfo) = - Row( - modifier = Modifier.fillMaxWidth(), - ) { - ModelCardContentField("Architecture", model.formattedArchitecture) - - Spacer(modifier = Modifier.weight(1f)) - - ModelCardContentField(model.formattedQuantization, model.formattedFileSize) - } - -@Composable -fun ModelCardContentDatesRow(model: ModelInfo) { - val dateFormatter = remember { SimpleDateFormat("MMM d", Locale.getDefault()) } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - // Added date - ModelCardContentField("Added", dateFormatter.format(Date(model.dateAdded))) - - // Last used date (if available) - model.dateLastUsed?.let { lastUsed -> - ModelCardContentField("Last used", dateFormatter.format(Date(lastUsed))) - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun ModelCardContentTagsSection(tags: List) = - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - tags.forEach { tag -> - AssistChip( - enabled = false, - onClick = { /* No action */ }, - label = { - Text( - text = tag, - style = MaterialTheme.typography.bodySmall, - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.Light, - color = MaterialTheme.colorScheme.onSurface - ) - } - ) - } - } - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun ModelCardContentLanguagesSections(languages: List) = - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - languages.mapNotNull { languageCodeToFlagEmoji(it) }.forEach { emoji -> - AssistChip( - enabled = false, - onClick = { /* No action */ }, - label = { - Text( - text = emoji, - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Light, - color = MaterialTheme.colorScheme.onSurface - ) - } - ) - } - } - -@Composable -fun ModelCardContentField(name: String, value: String) = - Row { - Text( - text = name, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = value, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Light, - fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.onSurface - ) - } diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ModelUnloadDialogHandler.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ModelUnloadDialogHandler.kt deleted file mode 100644 index c5e6fa3afd..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/components/ModelUnloadDialogHandler.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.arm.aiplayground.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.viewmodel.UnloadModelState - -/** - * Reusable component for handling model unloading dialogs - * - * @param [UnloadModelState]: - * - Hidden: default state without showing any UI - * - Confirming: show dismissible [UnloadModelDialog] and asks for user confirmation to unload current model - * - Unloading: show non-dismissible [UnloadModelDialog] while unloading model - * - Error: show [UnloadModelErrorDialog] to prompt error message to user - */ -@Composable -fun ModelUnloadDialogHandler( - message: String, - unloadModelState: UnloadModelState, - onUnloadConfirmed: (onNavigateBack: () -> Unit) -> Unit, - onUnloadDismissed: () -> Unit, - onNavigateBack: () -> Unit -) { - when (unloadModelState) { - is UnloadModelState.Confirming -> { - UnloadModelDialog( - message = message, - onConfirm = { - onUnloadConfirmed(onNavigateBack) - }, - onDismiss = onUnloadDismissed, - isUnloading = false, - ) - } - is UnloadModelState.Unloading -> { - UnloadModelDialog( - message = message, - onConfirm = { - onUnloadConfirmed(onNavigateBack) - }, - onDismiss = onUnloadDismissed, - isUnloading = true - ) - } - is UnloadModelState.Error -> { - UnloadModelErrorDialog( - errorMessage = unloadModelState.message, - onConfirm = { - onUnloadDismissed() - onNavigateBack() - }, - onDismiss = onUnloadDismissed - ) - } - is UnloadModelState.Hidden -> { - // Dialog not shown - } - } -} - -@Composable -private fun UnloadModelDialog( - message: String, - onConfirm: () -> Unit, - onDismiss: () -> Unit, - isUnloading: Boolean = false -) { - AlertDialog( - onDismissRequest = { - // Ignore dismiss requests while unloading the model - if (!isUnloading) onDismiss() - }, - title = { - Text("Confirm Exit") - }, - text = { - Column { - Text(message) - - if (isUnloading) { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Unloading model...") - } - } - } - }, - confirmButton = { - TextButton( - onClick = onConfirm, - enabled = !isUnloading - ) { - Text("Yes, Exit") - } - }, - dismissButton = { - TextButton( - onClick = onDismiss, - enabled = !isUnloading - ) { - Text("Cancel") - } - } - ) -} - -@Composable -private fun UnloadModelErrorDialog( - errorMessage: String, - onConfirm: () -> Unit, - onDismiss: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = "Error Unloading Model", - color = MaterialTheme.colorScheme.error - ) - }, - text = { - Column { - Text( - text = errorMessage, - style = MaterialTheme.typography.bodyMedium - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "You may need to restart the app if this problem persists.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { Text("Continue") } - }, - dismissButton = { - TextButton(onClick = onDismiss) { Text("Stay on Screen") } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/AnimatedNavHost.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/AnimatedNavHost.kt deleted file mode 100644 index 68a4126191..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/AnimatedNavHost.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.arm.aiplayground.ui.scaffold - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.currentBackStackEntryAsState - -@Composable -fun AnimatedNavHost( - navController: NavHostController, - startDestination: String, - modifier: Modifier = Modifier, - contentAlignment: Alignment = Alignment.Center, - route: String? = null, - builder: NavGraphBuilder.() -> Unit -) { - val currentNavBackStackEntry by navController.currentBackStackEntryAsState() - var previousNavBackStackEntry by remember { mutableStateOf(null) } - - LaunchedEffect(currentNavBackStackEntry) { - previousNavBackStackEntry = currentNavBackStackEntry - } - - NavHost( - navController = navController, - startDestination = startDestination, - modifier = modifier, - contentAlignment = contentAlignment, - route = route, - enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = tween( - durationMillis = 200, - easing = LinearOutSlowInEasing - ) - ) - }, - exitTransition = { - slideOutOfContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = tween( - durationMillis = 200, - easing = LinearOutSlowInEasing - ) - ) - }, - popEnterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = tween( - durationMillis = 200, - easing = LinearOutSlowInEasing - ) - ) - }, - popExitTransition = { - slideOutOfContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = tween( - durationMillis = 200, - easing = LinearOutSlowInEasing - ) - ) - }, - builder = builder - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/AppScaffold.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/AppScaffold.kt deleted file mode 100644 index f6016e37d9..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/AppScaffold.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.arm.aiplayground.ui.scaffold - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import com.arm.aiplayground.ui.scaffold.bottombar.BenchmarkBottomBar -import com.arm.aiplayground.ui.scaffold.bottombar.BottomBarConfig -import com.arm.aiplayground.ui.scaffold.bottombar.ConversationBottomBar -import com.arm.aiplayground.ui.scaffold.bottombar.ModelsBrowsingBottomBar -import com.arm.aiplayground.ui.scaffold.bottombar.ModelsDeletingBottomBar -import com.arm.aiplayground.ui.scaffold.bottombar.ModelsManagementBottomBar -import com.arm.aiplayground.ui.scaffold.bottombar.ModelsSearchingBottomBar -import com.arm.aiplayground.ui.scaffold.topbar.DefaultTopBar -import com.arm.aiplayground.ui.scaffold.topbar.ModelsBrowsingTopBar -import com.arm.aiplayground.ui.scaffold.topbar.NavigationIcon -import com.arm.aiplayground.ui.scaffold.topbar.PerformanceTopBar -import com.arm.aiplayground.ui.scaffold.topbar.ModelsManagementTopBar -import com.arm.aiplayground.ui.scaffold.topbar.TopBarConfig - -/** - * Configuration of both [TopBarConfig] and [BottomBarConfig] - */ -data class ScaffoldConfig( - val topBarConfig: TopBarConfig, - val bottomBarConfig: BottomBarConfig = BottomBarConfig.None -) - -/** - * Events called back from child screens - */ -sealed class ScaffoldEvent { - data class ShowSnackbar( - val message: String, - val duration: SnackbarDuration = SnackbarDuration.Short, - val withDismissAction: Boolean = false, - val actionLabel: String? = null, - val onAction: (() -> Unit)? = null - ) : ScaffoldEvent() - - data class ChangeTitle(val newTitle: String) : ScaffoldEvent() - - data class ShareText( - val text: String, - val title: String? = null, - val mimeType: String = "text/plain" - ) : ScaffoldEvent() -} - -@Composable -fun AppScaffold( - topBarconfig: TopBarConfig, - bottomBarConfig: BottomBarConfig = BottomBarConfig.None, - onScaffoldEvent: (ScaffoldEvent) -> Unit, - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, - content: @Composable (PaddingValues) -> Unit -) { - val topBar: @Composable () -> Unit = { - when (topBarconfig) { - is TopBarConfig.None -> {} - - is TopBarConfig.Default -> DefaultTopBar( - title = topBarconfig.title, - onNavigateBack = topBarconfig.navigationIcon.backAction, - onMenuOpen = topBarconfig.navigationIcon.menuAction, - ) - - is TopBarConfig.ModelsBrowsing -> ModelsBrowsingTopBar( - title = topBarconfig.title, - showTooltip = topBarconfig.showTooltip, - showManagingToggle = topBarconfig.showManagingToggle, - onToggleManaging = topBarconfig.onToggleManaging, - onNavigateBack = topBarconfig.navigationIcon.backAction, - onMenuOpen = topBarconfig.navigationIcon.menuAction - ) - - is TopBarConfig.ModelsDeleting -> DefaultTopBar( - title = topBarconfig.title, - titleColor = MaterialTheme.colorScheme.error, - navigationIconTint = MaterialTheme.colorScheme.error, - onQuit = topBarconfig.navigationIcon.quitAction - ) - - is TopBarConfig.ModelsManagement -> ModelsManagementTopBar( - title = topBarconfig.title, - storageMetrics = topBarconfig.storageMetrics, - onScaffoldEvent = onScaffoldEvent, - onNavigateBack = topBarconfig.navigationIcon.backAction, - ) - - is TopBarConfig.Performance -> PerformanceTopBar( - title = topBarconfig.title, - memoryMetrics = topBarconfig.memoryMetrics, - temperatureDisplay = topBarconfig.temperatureInfo, - onScaffoldEvent = onScaffoldEvent, - onNavigateBack = topBarconfig.navigationIcon.backAction, - onMenuOpen = topBarconfig.navigationIcon.menuAction, - ) - } - } - - val bottomBar: @Composable () -> Unit = { - when (val config = bottomBarConfig) { - is BottomBarConfig.None -> { /* No bottom bar */ } - - is BottomBarConfig.Models.Browsing -> { - ModelsBrowsingBottomBar( - isSearchingEnabled = config.isSearchingEnabled, - onToggleSearching = config.onToggleSearching, - sortingConfig = config.sorting, - filteringConfig = config.filtering, - runActionConfig = config.runAction - ) - } - - is BottomBarConfig.Models.Searching -> { - ModelsSearchingBottomBar( - textFieldState = config.textFieldState, - onQuitSearching = config.onQuitSearching, - onSearch = config.onSearch, - runActionConfig = config.runAction, - ) - } - - is BottomBarConfig.Models.Managing -> { - ModelsManagementBottomBar( - isDeletionEnabled = config.isDeletionEnabled, - onToggleDeleting = config.onToggleDeleting, - sortingConfig = config.sorting, - filteringConfig = config.filtering, - importingConfig = config.importing - ) - } - - is BottomBarConfig.Models.Deleting -> { - ModelsDeletingBottomBar(config) - } - - is BottomBarConfig.Benchmark -> { - BenchmarkBottomBar( - showShareFab = config.showShareFab, - engineIdle = config.engineIdle, - onShare = config.onShare, - onRerun = config.onRerun, - onClear = config.onClear, - showModelCard = config.showModelCard, - onToggleModelCard = config.onToggleModelCard, - ) - } - - is BottomBarConfig.Conversation -> { - ConversationBottomBar( - isReady = config.isEnabled, - textFieldState = config.textFieldState, - onSendClick = config.onSendClick, - showModelCard = config.showModelCard, - onToggleModelCard = config.onToggleModelCard, - onAttachPhotoClick = config.onAttachPhotoClick, - onAttachFileClick = config.onAttachFileClick, - onAudioInputClick = config.onAudioInputClick, - ) - } - } - } - - Scaffold( - topBar = topBar, - bottomBar = bottomBar, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - content = content - ) -} - -// Helper functions to obtain navigation actions if exist -private val NavigationIcon.menuAction: (() -> Unit)? - get() = (this as? NavigationIcon.Menu)?.onMenuOpen - -private val NavigationIcon.backAction: (() -> Unit)? - get() = (this as? NavigationIcon.Back)?.onNavigateBack - -private val NavigationIcon.quitAction: (() -> Unit)? - get() = (this as? NavigationIcon.Quit)?.onQuit diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/NavigationDrawer.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/NavigationDrawer.kt deleted file mode 100644 index d95a5d61f8..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/NavigationDrawer.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.arm.aiplayground.ui.scaffold - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.DrawerState -import androidx.compose.material3.DrawerValue -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalDrawerSheet -import androidx.compose.material3.ModalNavigationDrawer -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.APP_NAME -import com.arm.aiplayground.BuildConfig -import com.arm.aiplayground.navigation.AppDestinations -import com.arm.aiplayground.navigation.NavigationActions -import kotlinx.coroutines.launch - -/** - * App navigation drawer that provides access to different sections of the app. - * Gesture opening can be controlled based on current screen. - */ -@Composable -fun AppNavigationDrawer( - drawerState: DrawerState, - navigationActions: NavigationActions, - gesturesEnabled: Boolean, - currentRoute: String, - content: @Composable () -> Unit -) { - val coroutineScope = rememberCoroutineScope() - val configuration = LocalConfiguration.current - - // Calculate drawer width (60% of screen width) - val drawerWidth = (configuration.screenWidthDp * 0.6).dp - - // Handle back button to close drawer if open - BackHandler(enabled = drawerState.currentValue == DrawerValue.Open) { - coroutineScope.launch { - drawerState.close() - } - } - - ModalNavigationDrawer( - drawerState = drawerState, - gesturesEnabled = gesturesEnabled, - drawerContent = { - ModalDrawerSheet( - modifier = Modifier.width(drawerWidth) - ) { - DrawerContent( - currentRoute = currentRoute, - onNavigate = { destination -> - coroutineScope.launch { - drawerState.close() - destination() - } - }, - navigationActions = navigationActions - ) - } - }, - content = content - ) -} - -@Composable -private fun DrawerContent( - currentRoute: String, - onNavigate: ((Function0)) -> Unit, - navigationActions: NavigationActions, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp) - ) { - // App Header - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = APP_NAME, - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center - ) - - Text( - text = BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 4.dp) - ) - } - - HorizontalDivider() - - Spacer(modifier = Modifier.height(16.dp)) - -// // Main Navigation Items - // TODO-han.yin: add back once we add more features -// Text( -// text = "Features", -// style = MaterialTheme.typography.labelMedium, -// color = MaterialTheme.colorScheme.onSurfaceVariant, -// modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) -// ) - - DrawerNavigationItem( - icon = Icons.Default.Home, - label = "Models", - isSelected = currentRoute == AppDestinations.MODELS_ROUTE, - onClick = { - if (currentRoute != AppDestinations.MODELS_ROUTE) { - onNavigate { navigationActions.navigateToModelSelection() } - } else { - onNavigate { /* No-op: simply close drawer */ } - } - } - ) - - DrawerNavigationItem( - icon = Icons.Default.Settings, - label = "Settings", - isSelected = currentRoute == AppDestinations.SETTINGS_GENERAL_ROUTE, - onClick = { onNavigate { navigationActions.navigateToSettingsGeneral() } } - ) - } -} - -@Composable -private fun DrawerNavigationItem( - icon: ImageVector, - label: String, - isSelected: Boolean, - onClick: () -> Unit -) { - val backgroundColor = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surface - } - - val contentColor = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurface - } - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(vertical = 4.dp), - color = backgroundColor, - shape = MaterialTheme.shapes.small - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = label, - tint = contentColor, - modifier = Modifier.size(24.dp) - ) - - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - color = contentColor, - modifier = Modifier.padding(start = 16.dp) - ) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BenchmarkBottomBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BenchmarkBottomBar.kt deleted file mode 100644 index ab03accd42..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BenchmarkBottomBar.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Badge -import androidx.compose.material.icons.filled.ClearAll -import androidx.compose.material.icons.filled.Replay -import androidx.compose.material.icons.filled.Share -import androidx.compose.material.icons.outlined.Badge -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable - -@Composable -fun BenchmarkBottomBar( - engineIdle: Boolean, - showShareFab: Boolean, - onShare: () -> Unit, - onRerun: () -> Unit, - onClear: () -> Unit, - showModelCard: Boolean, - onToggleModelCard: (Boolean) -> Unit, -) { - val controlTint = - if (engineIdle) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - - BottomAppBar( - actions = { - IconButton(onClick = onRerun) { - Icon( - imageVector = Icons.Default.Replay, - contentDescription = "Run the benchmark again", - tint = controlTint - ) - } - - IconButton(onClick = onClear) { - Icon( - imageVector = Icons.Default.ClearAll, - contentDescription = "Clear benchmark results", - tint = controlTint - ) - } - - IconButton(onClick = { onToggleModelCard(!showModelCard) } ) { - Icon( - imageVector = if (showModelCard) Icons.Default.Badge else Icons.Outlined.Badge, - contentDescription = "${if (showModelCard) "Hide" else "Show"} model card" - ) - } - }, - floatingActionButton = { - // Only show FAB if the benchmark result is ready - AnimatedVisibility( - visible = showShareFab, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = onShare, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = "Share the benchmark results" - ) - } - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt deleted file mode 100644 index bca11e3150..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/BottomBarConfig.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.foundation.text.input.TextFieldState -import com.arm.aiplayground.data.model.ModelFilter -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.data.model.ModelSortOrder -import com.arm.aiplayground.viewmodel.PreselectedModelToRun - -/** - * [BottomAppBar] configurations - */ -sealed class BottomBarConfig { - - object None : BottomBarConfig() - - sealed class Models : BottomBarConfig() { - - data class Browsing( - val isSearchingEnabled: Boolean, - val onToggleSearching: () -> Unit, - val sorting: SortingConfig, - val filtering: FilteringConfig, - val runAction: RunActionConfig, - ) : BottomBarConfig() { - data class SortingConfig( - val isEnabled: Boolean, - val currentOrder: ModelSortOrder, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val selectOrder: (ModelSortOrder) -> Unit - ) - - data class FilteringConfig( - val isEnabled: Boolean, - val filters: Map, - val onToggleFilter: (ModelFilter, Boolean) -> Unit, - val onClearFilters: () -> Unit, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit - ) - } - - data class Searching( - val textFieldState: TextFieldState, - val onQuitSearching: () -> Unit, - val onSearch: (String) -> Unit, - val runAction: RunActionConfig, - ) : BottomBarConfig() - - data class Managing( - val isDeletionEnabled: Boolean, - val onToggleDeleting: () -> Unit, - val sorting: SortingConfig, - val filtering: FilteringConfig, - val importing: ImportConfig, - ) : BottomBarConfig() { - data class SortingConfig( - val isEnabled: Boolean, - val currentOrder: ModelSortOrder, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val selectOrder: (ModelSortOrder) -> Unit - ) - - data class FilteringConfig( - val isEnabled: Boolean, - val filters: Map, - val onToggleFilter: (ModelFilter, Boolean) -> Unit, - val onClearFilters: () -> Unit, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit - ) - - data class ImportConfig( - val showTooltip: Boolean, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val importFromLocal: () -> Unit, - val importFromHuggingFace: () -> Unit - ) - } - - data class Deleting( - val onQuitDeleting: () -> Unit, - val selectedModels: Map, - val selectAllFilteredModels: () -> Unit, - val clearAllSelectedModels: () -> Unit, - val deleteSelected: () -> Unit - ) : BottomBarConfig() - - data class RunActionConfig( - val showTooltip: Boolean, - val preselectedModelToRun: PreselectedModelToRun?, - val onClickRun: (PreselectedModelToRun) -> Unit, - ) - } - - data class Benchmark( - val showShareFab: Boolean, - val engineIdle: Boolean, - val onShare: () -> Unit, - val onRerun: () -> Unit, - val onClear: () -> Unit, - val showModelCard: Boolean, - val onToggleModelCard: (Boolean) -> Unit, - ) : BottomBarConfig() - - data class Conversation( - val isEnabled: Boolean, - val textFieldState: TextFieldState, - val onSendClick: () -> Unit, - val showModelCard: Boolean, - val onToggleModelCard: (Boolean) -> Unit, - val onAttachPhotoClick: (() -> Unit)? = null, - val onAttachFileClick: (() -> Unit)? = null, - val onAudioInputClick: (() -> Unit)? = null, - ) : BottomBarConfig() -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ConversationBottomBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ConversationBottomBar.kt deleted file mode 100644 index 9beb8816c0..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ConversationBottomBar.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.filled.Badge -import androidx.compose.material.icons.filled.Mic -import androidx.compose.material.icons.outlined.AddPhotoAlternate -import androidx.compose.material.icons.outlined.AttachFile -import androidx.compose.material.icons.outlined.Badge -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp - -@Composable -fun ConversationBottomBar( - textFieldState: TextFieldState, - isReady: Boolean, - onSendClick: () -> Unit, - showModelCard: Boolean, - onToggleModelCard: (Boolean) -> Unit, - onAttachPhotoClick: (() -> Unit)?, - onAttachFileClick: (() -> Unit)?, - onAudioInputClick: (() -> Unit)?, -) { - val placeholder = if (isReady) "Type your message here" else "AI is generating the response..." - - Column( - modifier = Modifier.fillMaxWidth() - ) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = BottomAppBarDefaults.containerColor, - tonalElevation = BottomAppBarDefaults.ContainerElevation, - shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) - ) { - Box( - modifier = Modifier.fillMaxWidth() - .padding(start = 16.dp, top = 16.dp, end = 16.dp), - ) { - OutlinedTextField( - state = textFieldState, - modifier = Modifier.fillMaxWidth(), - enabled = isReady, - placeholder = { Text(placeholder) }, - lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5), - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy( - alpha = 0.5f - ), - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surfaceDim, - ), - shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), - onKeyboardAction = { - if (isReady) { - onSendClick() - } - } - ) - } - } - - BottomAppBar( - actions = { - onAttachPhotoClick?.let { - IconButton(onClick = it) { - Icon( - imageVector = Icons.Outlined.AddPhotoAlternate, - contentDescription = "Attach a photo", - ) - } - } - - onAttachFileClick?.let { - IconButton(onClick = it) { - Icon( - imageVector = Icons.Outlined.AttachFile, - contentDescription = "Attach a file", - ) - } - } - - onAudioInputClick?.let { - IconButton(onClick = it) { - Icon( - imageVector = Icons.Default.Mic, - contentDescription = "Input with voice", - ) - } - } - - IconButton(onClick = { onToggleModelCard(!showModelCard) } ) { - Icon( - imageVector = if (showModelCard) Icons.Default.Badge else Icons.Outlined.Badge, - contentDescription = "${if (showModelCard) "Hide" else "Show"} model card" - ) - } - }, - floatingActionButton = { - FloatingActionButton( - onClick = { if (isReady) { onSendClick() } }, - containerColor = MaterialTheme.colorScheme.primary, - ) { - if (isReady) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Send, - contentDescription = "Send message", - ) - } else { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeCap = StrokeCap.Round, - ) - } - } - } - ) - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt deleted file mode 100644 index b750908c36..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt +++ /dev/null @@ -1,222 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Sort -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.outlined.FilterAlt -import androidx.compose.material.icons.outlined.FilterAltOff -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.Text -import androidx.compose.material3.TooltipAnchorPosition -import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.rememberTooltipState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.data.model.ModelSortOrder - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModelsBrowsingBottomBar( - isSearchingEnabled: Boolean, - onToggleSearching: () -> Unit, - sortingConfig: BottomBarConfig.Models.Browsing.SortingConfig, - filteringConfig: BottomBarConfig.Models.Browsing.FilteringConfig, - runActionConfig: BottomBarConfig.Models.RunActionConfig, -) { - val tooltipState = rememberTooltipState( - initialIsVisible = runActionConfig.showTooltip, - isPersistent = runActionConfig.showTooltip - ) - - LaunchedEffect(runActionConfig.preselectedModelToRun) { - if (runActionConfig.showTooltip && runActionConfig.preselectedModelToRun != null) { - tooltipState.show() - } - } - - BottomAppBar( - actions = { - // Enter search action - IconButton( - enabled = isSearchingEnabled, - onClick = onToggleSearching - ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search models" - ) - } - - // Sorting action - IconButton( - enabled = sortingConfig.isEnabled, - onClick = { sortingConfig.toggleMenu(true) } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Sort, - contentDescription = "Sort models" - ) - } - - // Sorting dropdown menu - DropdownMenu( - expanded = sortingConfig.isMenuVisible, - onDismissRequest = { sortingConfig.toggleMenu(false) } - ) { - val sortOptions = listOf( - Triple( - ModelSortOrder.NAME_ASC, - "Name (A-Z)", - "Sort by name in ascending order" - ), - Triple( - ModelSortOrder.NAME_DESC, - "Name (Z-A)", - "Sort by name in descending order" - ), - Triple( - ModelSortOrder.SIZE_ASC, - "Size (Smallest first)", - "Sort by size in ascending order" - ), - Triple( - ModelSortOrder.SIZE_DESC, - "Size (Largest first)", - "Sort by size in descending order" - ), - Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") - ) - - sortOptions.forEach { (order, label, description) -> - DropdownMenuItem( - text = { Text(label) }, - trailingIcon = { - if (sortingConfig.currentOrder == order) - Icon( - imageVector = Icons.Default.Check, - contentDescription = "$description, selected" - ) - }, - onClick = { sortingConfig.selectOrder(order) } - ) - } - } - - // Filter action - val hasFilters = filteringConfig.filters.any { it.value } - IconButton( - enabled = filteringConfig.isEnabled, - colors = IconButtonDefaults.iconButtonColors().copy( - contentColor = if (hasFilters) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant - ), - onClick = { filteringConfig.toggleMenu(true) } - ) { - Icon( - imageVector = - if (hasFilters) Icons.Default.FilterAlt - else Icons.Outlined.FilterAlt, - contentDescription = "Filter models", - ) - } - - // Filter dropdown menu - DropdownMenu( - expanded = filteringConfig.isMenuVisible, - onDismissRequest = { filteringConfig.toggleMenu(false) } - ) { - Text( - text = "Filter by", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - filteringConfig.filters.forEach { (filter, isEnabled) -> - DropdownMenuItem( - text = { Text(filter.displayName) }, - leadingIcon = { - Checkbox( - checked = isEnabled, - onCheckedChange = null - ) - }, - onClick = { filteringConfig.onToggleFilter(filter, !isEnabled) } - ) - } - - HorizontalDivider() - - DropdownMenuItem( - text = { Text("Clear filters") }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.FilterAltOff, - contentDescription = "Clear all filters" - ) - }, - onClick = { - filteringConfig.onClearFilters() - filteringConfig.toggleMenu(false) - } - ) - } - }, - floatingActionButton = { - TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider( - TooltipAnchorPosition.Above), - state = tooltipState, - tooltip = { - PlainTooltip { - Text("Tap this button to run your first model!") - } - }, - onDismissRequest = {} - ) { - // Only show FAB if a model is selected - AnimatedVisibility( - visible = runActionConfig.preselectedModelToRun != null, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = { - runActionConfig.preselectedModelToRun?.let { - runActionConfig.onClickRun(it) - } - }, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Run with selected model", - ) - } - } - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsDeletingBottomBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsDeletingBottomBar.kt deleted file mode 100644 index e256e831d2..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsDeletingBottomBar.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ClearAll -import androidx.compose.material.icons.filled.DeleteForever -import androidx.compose.material.icons.filled.SelectAll -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable - - -@Composable -fun ModelsDeletingBottomBar( - deleting: BottomBarConfig.Models.Deleting, -) { - BottomAppBar( - actions = { - IconButton(onClick = { deleting.clearAllSelectedModels() }) { - Icon( - imageVector = Icons.Default.ClearAll, - contentDescription = "Deselect all" - ) - } - - IconButton(onClick = { deleting.selectAllFilteredModels() }) { - Icon( - imageVector = Icons.Default.SelectAll, - contentDescription = "Select all" - ) - } - }, - floatingActionButton = { - AnimatedVisibility( - visible = deleting.selectedModels.isNotEmpty(), - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = { - deleting.deleteSelected() - }, - containerColor = MaterialTheme.colorScheme.error - ) { - Icon( - imageVector = Icons.Default.DeleteForever, - contentDescription = "Delete selected models", - tint = MaterialTheme.colorScheme.onError, - ) - } - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsManagementBottomBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsManagementBottomBar.kt deleted file mode 100644 index 9e348286c0..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsManagementBottomBar.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Sort -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.FolderOpen -import androidx.compose.material.icons.outlined.DeleteSweep -import androidx.compose.material.icons.outlined.FilterAlt -import androidx.compose.material.icons.outlined.FilterAltOff -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.Text -import androidx.compose.material3.TooltipAnchorPosition -import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.rememberTooltipState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.R -import com.arm.aiplayground.data.model.ModelSortOrder - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModelsManagementBottomBar( - isDeletionEnabled: Boolean, - onToggleDeleting: () -> Unit, - sortingConfig: BottomBarConfig.Models.Managing.SortingConfig, - filteringConfig: BottomBarConfig.Models.Managing.FilteringConfig, - importingConfig: BottomBarConfig.Models.Managing.ImportConfig, -) { - val tooltipState = rememberTooltipState( - initialIsVisible = false, - isPersistent = importingConfig.showTooltip) - - LaunchedEffect(importingConfig) { - if (importingConfig.showTooltip && !importingConfig.isMenuVisible) { - tooltipState.show() - } - } - - BottomAppBar( - actions = { - // Batch-deletion action - IconButton(enabled = isDeletionEnabled, onClick = onToggleDeleting) { - Icon( - imageVector = Icons.Outlined.DeleteSweep, - contentDescription = "Delete models",) - } - - // Sorting action - IconButton( - enabled = sortingConfig.isEnabled, - onClick = { sortingConfig.toggleMenu(true) } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Sort, - contentDescription = "Sort models" - ) - } - - // Sorting dropdown menu - DropdownMenu( - expanded = sortingConfig.isMenuVisible, - onDismissRequest = { sortingConfig.toggleMenu(false) } - ) { - val sortOptions = listOf( - Triple( - ModelSortOrder.NAME_ASC, - "Name (A-Z)", - "Sort by name in ascending order" - ), - Triple( - ModelSortOrder.NAME_DESC, - "Name (Z-A)", - "Sort by name in descending order" - ), - Triple( - ModelSortOrder.SIZE_ASC, - "Size (Smallest first)", - "Sort by size in ascending order" - ), - Triple( - ModelSortOrder.SIZE_DESC, - "Size (Largest first)", - "Sort by size in descending order" - ), - Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") - ) - - sortOptions.forEach { (order, label, description) -> - DropdownMenuItem( - text = { Text(label) }, - trailingIcon = { - if (sortingConfig.currentOrder == order) - Icon( - imageVector = Icons.Default.Check, - contentDescription = "$description, selected" - ) - }, - onClick = { sortingConfig.selectOrder(order) } - ) - } - } - - // Filtering action - val hasFilters = filteringConfig.filters.any { it.value } - IconButton( - enabled = filteringConfig.isEnabled, - onClick = { filteringConfig.toggleMenu(true) }, - colors = IconButtonDefaults.iconButtonColors().copy( - contentColor = if (hasFilters) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant - ), - ) { - Icon( - imageVector = - if (hasFilters) Icons.Default.FilterAlt - else Icons.Outlined.FilterAlt, - contentDescription = "Filter models", - ) - } - - // Filter dropdown menu - DropdownMenu( - expanded = filteringConfig.isMenuVisible, - onDismissRequest = { filteringConfig.toggleMenu(false) } - ) { - Text( - text = "Filter by", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - filteringConfig.filters.forEach { (filter, isEnabled) -> - DropdownMenuItem( - text = { Text(filter.displayName) }, - leadingIcon = { - Checkbox( - checked = isEnabled, - onCheckedChange = null - ) - }, - onClick = { filteringConfig.onToggleFilter(filter, !isEnabled) } - ) - } - - HorizontalDivider() - - DropdownMenuItem( - text = { Text("Clear filters") }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.FilterAltOff, - contentDescription = "Clear all filters" - ) - }, - onClick = { - filteringConfig.onClearFilters() - filteringConfig.toggleMenu(false) - } - ) - } - }, - floatingActionButton = { - TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider( - TooltipAnchorPosition.Above), - state = tooltipState, - tooltip = { - PlainTooltip { - Text("Tap this button to install your first model!") - } - }, - onDismissRequest = {} - ) { - FloatingActionButton( - onClick = { importingConfig.toggleMenu(true) }, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add model" - ) - } - } - - // Add model dropdown menu - DropdownMenu( - expanded = importingConfig.isMenuVisible, - onDismissRequest = { importingConfig.toggleMenu(false) } - ) { - DropdownMenuItem( - text = { Text("Import a local model") }, - leadingIcon = { - Icon( - imageVector = Icons.Default.FolderOpen, - contentDescription = "Import a local model on the device" - ) - }, - onClick = importingConfig.importFromLocal - ) - DropdownMenuItem( - text = { Text("Download from Hugging Face") }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.logo_huggingface), - contentDescription = "Browse and download a model from HuggingFace", - modifier = Modifier.size(24.dp), - tint = Color.Unspecified, - ) - }, - onClick = importingConfig.importFromHuggingFace - ) - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsSearchingBottomBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsSearchingBottomBar.kt deleted file mode 100644 index 8b1cf124b2..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/bottombar/ModelsSearchingBottomBar.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.bottombar - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Backspace -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.SearchOff -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable - -@Composable -fun ModelsSearchingBottomBar( - textFieldState: TextFieldState, - onQuitSearching: () -> Unit, - onSearch: (String) -> Unit, // TODO-han.yin: somehow this is unused? - runActionConfig: BottomBarConfig.Models.RunActionConfig, -) { - BottomAppBar( - actions = { - // Quit search action - IconButton(onClick = onQuitSearching) { - Icon( - imageVector = Icons.Default.SearchOff, - contentDescription = "Quit search mode" - ) - } - - // Clear query action - IconButton(onClick = { textFieldState.clearText() }) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.Backspace, - contentDescription = "Clear query text" - ) - } - }, - floatingActionButton = { - // Only show FAB if a model is selected - AnimatedVisibility( - visible = runActionConfig.preselectedModelToRun != null, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = { - runActionConfig.preselectedModelToRun?.let { - runActionConfig.onClickRun(it) - } - }, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Run with selected model" - ) - } - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/DefaultTopBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/DefaultTopBar.kt deleted file mode 100644 index c573317e64..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/DefaultTopBar.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.topbar - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DefaultTopBar( - title: String, - titleColor: Color = Color.Unspecified, - navigationIconTint: Color = LocalContentColor.current, - onNavigateBack: (() -> Unit)? = null, - onQuit: (() -> Unit)? = null, - onMenuOpen: (() -> Unit)? = null -) { - TopAppBar( - title = { - Text(text = title, color = titleColor) - }, - navigationIcon = { - when { - onQuit != null -> { - IconButton(onClick = onQuit) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Quit", - tint = navigationIconTint - ) - } - } - - onNavigateBack != null -> { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - tint = navigationIconTint - ) - } - } - - onMenuOpen != null -> { - IconButton(onClick = onMenuOpen) { - Icon( - imageVector = Icons.Default.Menu, - contentDescription = "Menu", - tint = navigationIconTint - ) - } - } - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/ModelsBrowsingTopBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/ModelsBrowsingTopBar.kt deleted file mode 100644 index c784464004..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/ModelsBrowsingTopBar.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.topbar - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Build -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.Text -import androidx.compose.material3.TooltipAnchorPosition -import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTooltipState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModelsBrowsingTopBar( - title: String, - showTooltip: Boolean, - showManagingToggle: Boolean, - onToggleManaging: () -> Unit, - onNavigateBack: (() -> Unit)? = null, - onMenuOpen: (() -> Unit)? = null, -) { - val tooltipState = rememberTooltipState( - initialIsVisible = showTooltip, - isPersistent = showTooltip - ) - - LaunchedEffect(showTooltip, showManagingToggle) { - if (showTooltip && showManagingToggle) { - tooltipState.show() - } - } - - TopAppBar( - title = { Text(title) }, - navigationIcon = { - when { - onNavigateBack != null -> { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - } - - onMenuOpen != null -> { - IconButton(onClick = onMenuOpen) { - Icon( - imageVector = Icons.Default.Menu, - contentDescription = "Menu" - ) - } - } - } - }, - actions = { - if (showManagingToggle) { - TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider( - TooltipAnchorPosition.Below), - state = tooltipState, - tooltip = { - PlainTooltip { - Text("Tap this button to install another model or manage your models!") - } - }, - onDismissRequest = {} - ) { - ModelManageActionToggle(onToggleManaging) - } - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) - ) -} - -@Composable -private fun ModelManageActionToggle( - onToggleManaging: () -> Unit, -) { - FilledTonalButton( - modifier = Modifier.padding(end = 8.dp), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - onClick = onToggleManaging - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Build, - contentDescription = "Manage models", - tint = MaterialTheme.colorScheme.onSurface, - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = "Manage", - style = MaterialTheme.typography.bodySmall - ) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/ModelsManagementTopBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/ModelsManagementTopBar.kt deleted file mode 100644 index 234af896ff..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/ModelsManagementTopBar.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.topbar - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.SdStorage -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.monitoring.StorageMetrics -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import java.util.Locale - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModelsManagementTopBar( - title: String, - storageMetrics: StorageMetrics?, - onScaffoldEvent: (ScaffoldEvent) -> Unit, - onNavigateBack: (() -> Unit)? = null, -) { - TopAppBar( - title = { Text(title) }, - navigationIcon = { - if (onNavigateBack != null) { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - } - }, - actions = { - storageMetrics?.let { - StorageIndicator(storageMetrics = it, onScaffoldEvent = onScaffoldEvent) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) - ) -} - -@Composable -private fun StorageIndicator( - storageMetrics: StorageMetrics, - onScaffoldEvent: (ScaffoldEvent) -> Unit, -) { - - val usedGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.usedGB) - val availableGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.availableGB) - - OutlinedButton( - modifier = Modifier.padding(end = 8.dp), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - onClick = { - onScaffoldEvent(ScaffoldEvent.ShowSnackbar( - message = "Your models occupy $usedGb GB storage\nRemaining free space available: $availableGb GB", - withDismissAction = true, - )) - } - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.SdStorage, - contentDescription = "Storage", - tint = when { - storageMetrics.availableGB < 5.0f -> MaterialTheme.colorScheme.error - storageMetrics.availableGB < 10.0f -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) - else -> MaterialTheme.colorScheme.onSurface - } - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = "$usedGb / $availableGb GB", - style = MaterialTheme.typography.bodySmall - ) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/PerformanceTopBar.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/PerformanceTopBar.kt deleted file mode 100644 index 89e867995e..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/PerformanceTopBar.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.topbar - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Memory -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.Thermostat -import androidx.compose.material.icons.filled.WarningAmber -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.monitoring.MemoryMetrics -import com.arm.aiplayground.monitoring.TemperatureMetrics -import com.arm.aiplayground.monitoring.TemperatureWarningLevel -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import java.util.Locale - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PerformanceTopBar( - title: String, - memoryMetrics: MemoryMetrics?, - temperatureDisplay: Pair?, - onScaffoldEvent: (ScaffoldEvent) -> Unit, - onNavigateBack: (() -> Unit)? = null, - onMenuOpen: (() -> Unit)? = null, -) { - TopAppBar( - title = { Text(title) }, - navigationIcon = { - when { - onNavigateBack != null -> { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - } - - onMenuOpen != null -> { - IconButton(onClick = onMenuOpen) { - Icon( - imageVector = Icons.Default.Menu, - contentDescription = "Menu" - ) - } - } - } - }, - actions = { - // Temperature indicator (optional) - temperatureDisplay?.let { (temperatureMetrics, useFahrenheit) -> - TemperatureIndicator( - temperatureMetrics = temperatureMetrics, - useFahrenheit = useFahrenheit, - onScaffoldEvent = onScaffoldEvent, - ) - } - - // Memory indicator - memoryMetrics?.let { - MemoryIndicator(memoryUsage = it, onScaffoldEvent = onScaffoldEvent) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) - ) -} - -@Composable -private fun MemoryIndicator( - memoryUsage: MemoryMetrics, - onScaffoldEvent: (ScaffoldEvent) -> Unit, -) { - val availableGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.availableGB) - val totalGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.totalGB) - - OutlinedButton( - modifier = Modifier.padding(end = 8.dp), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - onClick = { - onScaffoldEvent(ScaffoldEvent.ShowSnackbar( - message = "Free RAM available: $availableGB GB\nTotal RAM on your device: $totalGB GB", - withDismissAction = true, - )) - } - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Memory, - contentDescription = "RAM usage", - tint = when { - memoryUsage.availableGB < 1 -> MaterialTheme.colorScheme.error - memoryUsage.availableGB < 3 -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) - - Text( - modifier = Modifier.padding(start = 2.dp), - text = "$availableGB / $totalGB GB", - style = MaterialTheme.typography.bodySmall, - ) - } - } -} - -@Composable -private fun TemperatureIndicator( - temperatureMetrics: TemperatureMetrics, - useFahrenheit: Boolean, - onScaffoldEvent: (ScaffoldEvent) -> Unit, -) { - val temperatureDisplay = temperatureMetrics.getDisplay(useFahrenheit) - - val temperatureWarning = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> "Your device is HEATED UP to $temperatureDisplay, please cool it down before continue using the app." - TemperatureWarningLevel.MEDIUM -> "Your device is warming up to $temperatureDisplay." - else -> "Your device's temperature is $temperatureDisplay." - } - val warningDismissible = temperatureMetrics.warningLevel != TemperatureWarningLevel.HIGH - - OutlinedButton( - modifier = Modifier.padding(end = 8.dp), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - onClick = { - onScaffoldEvent(ScaffoldEvent.ShowSnackbar( - message = temperatureWarning, - withDismissAction = warningDismissible, - )) - } - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> Icons.Default.WarningAmber - else -> Icons.Default.Thermostat - }, - contentDescription = "Device temperature", - tint = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error - TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = temperatureDisplay, - style = MaterialTheme.typography.bodySmall, - color = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error - TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/TopBarConfig.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/TopBarConfig.kt deleted file mode 100644 index 9586d6b196..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/scaffold/topbar/TopBarConfig.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.arm.aiplayground.ui.scaffold.topbar - -import com.arm.aiplayground.monitoring.MemoryMetrics -import com.arm.aiplayground.monitoring.StorageMetrics -import com.arm.aiplayground.monitoring.TemperatureMetrics - -/** - * [TopAppBar] configurations - */ -sealed class TopBarConfig { - abstract val title: String - abstract val navigationIcon: NavigationIcon - - // No top bar at all - data class None( - override val title: String = "", - override val navigationIcon: NavigationIcon = NavigationIcon.None - ) : TopBarConfig() - - // Default/simple top bar with only a navigation icon - data class Default( - override val title: String, - override val navigationIcon: NavigationIcon - ) : TopBarConfig() - - // Model management top bar with a toggle to turn on/off manage mode - data class ModelsBrowsing( - override val title: String, - override val navigationIcon: NavigationIcon, - val showTooltip: Boolean, - val showManagingToggle: Boolean, - val onToggleManaging: () -> Unit, - ) : TopBarConfig() - - // Model batch-deletion top bar with a toggle to turn on/off manage mode - data class ModelsDeleting( - override val title: String, - override val navigationIcon: NavigationIcon, - ) : TopBarConfig() - - // Performance monitoring top bar with RAM and optional temperature - data class Performance( - override val title: String, - override val navigationIcon: NavigationIcon, - val memoryMetrics: MemoryMetrics?, - val temperatureInfo: Pair?, - ) : TopBarConfig() - - // Storage management top bar with used & total storage - data class ModelsManagement( - override val title: String, - override val navigationIcon: NavigationIcon, - val storageMetrics: StorageMetrics? - ) : TopBarConfig() -} - -/** - * Helper class for navigation icon configuration - */ -sealed class NavigationIcon { - data class Menu(val onMenuOpen: () -> Unit) : NavigationIcon() - data class Back(val onNavigateBack: () -> Unit) : NavigationIcon() - data class Quit(val onQuit: () -> Unit) : NavigationIcon() - data object None : NavigationIcon() -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/BenchmarkScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/BenchmarkScreen.kt deleted file mode 100644 index af55d28ac1..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/BenchmarkScreen.kt +++ /dev/null @@ -1,344 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import android.content.Intent -import android.widget.Toast -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Replay -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.ProgressIndicatorDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import com.arm.aichat.InferenceEngine.State -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.engine.ModelLoadingMetrics -import com.arm.aiplayground.ui.components.ModelCardContentArchitectureRow -import com.arm.aiplayground.ui.components.ModelCardContentContextRow -import com.arm.aiplayground.ui.components.ModelCardContentField -import com.arm.aiplayground.ui.components.ModelCardCoreExpandable -import com.arm.aiplayground.ui.components.ModelUnloadDialogHandler -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import com.arm.aiplayground.util.TableData -import com.arm.aiplayground.util.formatMilliSeconds -import com.arm.aiplayground.util.parseMarkdownTable -import com.arm.aiplayground.viewmodel.BenchmarkResult -import com.arm.aiplayground.viewmodel.BenchmarkViewModel - - -@Composable -fun BenchmarkScreen( - loadingMetrics: ModelLoadingMetrics, - onScaffoldEvent: (ScaffoldEvent) -> Unit, - onNavigateBack: () -> Unit, - viewModel: BenchmarkViewModel -) { - val context = LocalContext.current - - // View model states - val engineState by viewModel.engineState.collectAsState() - val unloadDialogState by viewModel.unloadModelState.collectAsState() - - val showModelCard by viewModel.showModelCard.collectAsState() - val selectedModel by viewModel.selectedModel.collectAsState() - - val benchmarkResults by viewModel.benchmarkResults.collectAsState() - - // UI states - var isModelCardExpanded by remember { mutableStateOf(true) } - var isInitialBenchmarkRun by rememberSaveable { mutableStateOf(false) } - - // Run benchmark when entering the screen - LaunchedEffect(selectedModel) { - if (!isInitialBenchmarkRun) { - isInitialBenchmarkRun = true - viewModel.runBenchmark() - } - } - - // Handle back button press - BackHandler { - viewModel.onBackPressed(onNavigateBack) - } - - val onInfo = { - Toast.makeText(context, "Please refer to this post for more details on the benchmark methodology", Toast.LENGTH_SHORT).show() - val intent = Intent(Intent.ACTION_VIEW, "https://blog.steelph0enix.dev/posts/llama-cpp-guide/#llama-bench".toUri()) - context.startActivity(intent) - } - - Box( - modifier = Modifier.fillMaxSize() - ) { - // Benchmark results - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.Bottom, - ) { - items(items = benchmarkResults) { - BenchmarkResultCard( - result = it, - onRerun = { viewModel.rerunBenchmark(onScaffoldEvent) }, - onInfo = onInfo, - ) - } - } - - // Loading indicator - if (engineState is State.Benchmarking) { - Card( - modifier = Modifier.align(Alignment.Center), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - shape = MaterialTheme.shapes.extraLarge - ) { - Column( - modifier = Modifier.padding(horizontal = 32.dp, vertical = 48.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator( - modifier = Modifier.size(64.dp), - strokeWidth = ProgressIndicatorDefaults.CircularStrokeWidth * 1.5f - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Running benchmark...", - style = MaterialTheme.typography.headlineSmall - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "This usually takes a few minutes", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - // Selected model card and loading metrics - if (showModelCard) { - selectedModel?.let { model -> - Box(modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)) { - ModelCardWithLoadingMetrics( - model = model, - loadingMetrics = loadingMetrics, - isExpanded = isModelCardExpanded, - onExpanded = { isModelCardExpanded = !isModelCardExpanded }, - ) - } - } - } - } - - // Unload confirmation dialog - ModelUnloadDialogHandler( - message = "Going back will unload the current model and clear all the benchmark results.", - unloadModelState = unloadDialogState, - onUnloadConfirmed = { viewModel.onUnloadConfirmed(onNavigateBack) }, - onUnloadDismissed = { viewModel.onUnloadDismissed() }, - onNavigateBack = onNavigateBack, - ) -} - -@Composable -private fun ModelCardWithLoadingMetrics( - model: ModelInfo, - loadingMetrics: ModelLoadingMetrics, - isExpanded: Boolean = false, - onExpanded: ((Boolean) -> Unit)? = null, -) = ModelCardCoreExpandable( - model = model, - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - isExpanded = isExpanded, - onExpanded = onExpanded -) { - Spacer(modifier = Modifier.height(8.dp)) - - // Row 2: Context length, size label - ModelCardContentContextRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 3: Architecture, quantization, formatted size - ModelCardContentArchitectureRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 4: Model loading time - ModelCardContentField("Loading time", formatMilliSeconds(loadingMetrics.modelLoadingTimeMs)) -} - - -@Composable -fun BenchmarkResultCard( - result: BenchmarkResult, - onRerun: () -> Unit, - onInfo: () -> Unit, -) { - val rawTable = parseMarkdownTable(result.text.trimIndent()) - val model = rawTable.getColumn("model").firstOrNull() ?: "Unknown" - val parameters = rawTable.getColumn("params").firstOrNull() ?: "-" - val size = rawTable.getColumn("size").firstOrNull() ?: "-" - - Card( - modifier = Modifier.fillMaxWidth().padding(8.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp) - ) - .padding(16.dp) - ) { - Row { - Text( - text = "Model", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Normal, - ) - - Spacer(modifier = Modifier.width(16.dp)) - - Text( - modifier = Modifier.weight(1f), - text = model, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Light, - fontStyle = FontStyle.Italic, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - Row { - ModelCardContentField("Parameters", parameters) - - Spacer(modifier = Modifier.weight(1f)) - - ModelCardContentField("Size", size) - } - - BenchmarkResultTable(rawTable) - - ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration)) - - Spacer(modifier = Modifier.height(8.dp)) - - Row { - OutlinedButton(onClick = onRerun) { - Icon( - imageVector = Icons.Default.Replay, - contentDescription = "Run the benchmark again" - ) - Text("Run again", modifier = Modifier.padding(start = 6.dp)) - } - - Spacer(modifier = Modifier.weight(1f)) - - FilledTonalButton(onClick = onInfo) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = "Information about what the result means" - ) - Text("How to interpret", modifier = Modifier.padding(start = 6.dp)) - } - } - } - } -} - -// Needs to be aligned with `bench` implementation -private val COLUMNS_TO_KEEP = setOf("backend", "test", "t/s") -private val WEIGHTS_EACH_COLUMN = listOf(1f, 1f, 2f) - -@Composable -fun BenchmarkResultTable( - rawTable: TableData, - columnsToKeep: Set = COLUMNS_TO_KEEP, - columnWeights: List = WEIGHTS_EACH_COLUMN -) { - val (headers, rows) = rawTable.filterColumns(columnsToKeep) - - Column( - modifier = Modifier - .padding(horizontal = 12.dp, vertical = 16.dp) - .border(1.dp, MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(4.dp)) - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - BenchmarkResultTableRow(headers, columnWeights, isHeader = true) - HorizontalDivider(thickness = 1.dp) - rows.forEach { BenchmarkResultTableRow(it, columnWeights) } - } -} - -@Composable -fun BenchmarkResultTableRow( - cells: List, - weights: List? = null, - isHeader: Boolean = false, -) { - val effectiveWeights = weights ?: List(cells.size) { 1f } - - Row(modifier = Modifier.fillMaxWidth()) { - cells.forEachIndexed { index, cell -> - Text( - modifier = Modifier.weight(effectiveWeights.getOrElse(index) { 1f }), - text = cell, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - fontWeight = if (isHeader) FontWeight.Normal else FontWeight.Light, - fontStyle = if (isHeader) FontStyle.Normal else FontStyle.Italic - ) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ConversationScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ConversationScreen.kt deleted file mode 100644 index 8483899cd3..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ConversationScreen.kt +++ /dev/null @@ -1,614 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import android.content.Intent -import android.widget.Toast -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline -import androidx.compose.material.icons.filled.Timer -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.FilterChip -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner -import com.arm.aichat.InferenceEngine.State -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.engine.ModelLoadingMetrics -import com.arm.aiplayground.engine.TokenMetrics -import com.arm.aiplayground.ui.components.ModelCardContentArchitectureRow -import com.arm.aiplayground.ui.components.ModelCardContentContextRow -import com.arm.aiplayground.ui.components.ModelCardContentField -import com.arm.aiplayground.ui.components.ModelCardCoreExpandable -import com.arm.aiplayground.ui.components.ModelUnloadDialogHandler -import com.arm.aiplayground.util.formatMilliSeconds -import com.arm.aiplayground.util.formatMilliSecondstructured -import com.arm.aiplayground.util.toEnglishName -import com.arm.aiplayground.viewmodel.ConversationViewModel -import com.arm.aiplayground.viewmodel.Message -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.launch -import java.util.Locale - - -/** - * Screen for LLM conversation with user. - */ -@OptIn(FlowPreview::class) -@Composable -fun ConversationScreen( - loadingMetrics: ModelLoadingMetrics, - onNavigateBack: () -> Unit, - viewModel: ConversationViewModel -) { - // View model states - val engineState by viewModel.engineState.collectAsState() - val unloadDialogState by viewModel.unloadModelState.collectAsState() - - val showModelCard by viewModel.showModelCard.collectAsState() - val selectedModel by viewModel.selectedModel.collectAsState() - val systemPrompt by viewModel.systemPrompt.collectAsState() - - val messages by viewModel.messages.collectAsState() - - val isGenerating = engineState is State.Generating - - // UI states - val lifecycleOwner = LocalLifecycleOwner.current - val coroutineScope = rememberCoroutineScope() - var isModelCardExpanded by remember { mutableStateOf(true) } - val listState = rememberLazyListState() - - // Track the actual rendered size of the last message bubble - var lastMessageBubbleHeight by remember { mutableIntStateOf(0) } - - // Track if user has manually scrolled up - var userHasScrolledUp by remember { mutableStateOf(false) } - - // Detect if user is at the very bottom - val isAtBottom = remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() - - // At bottom if we can see the bottom spacer - lastVisibleItem != null && lastVisibleItem.index == messages.size - } - } - - // Reset scroll flag when user returns to bottom - LaunchedEffect(Unit) { - snapshotFlow { - listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == messages.size - } - .distinctUntilChanged() - .filter { it } - .collect { userHasScrolledUp = false } - } - - // Then scroll to bottom spacer instead - LaunchedEffect(isGenerating) { - snapshotFlow { - listState.layoutInfo.visibleItemsInfo.find { - it.index == messages.size - 1 - }?.size ?: 0 - } - .distinctUntilChanged() - .collect { currentHeight -> - // Only reposition if: - if (currentHeight > lastMessageBubbleHeight // 1. Height increased (new line) - && !userHasScrolledUp // 2. User hasn't scrolled up - && isAtBottom.value // 3. User is at bottom - ) { - lastMessageBubbleHeight = currentHeight - listState.scrollToItem(index = messages.size, scrollOffset = 0) - } else if (currentHeight > 0) { - lastMessageBubbleHeight = currentHeight - } - } - } - - // Detect manual scrolling - LaunchedEffect(listState.isScrollInProgress) { - if (listState.isScrollInProgress && !isAtBottom.value) { - userHasScrolledUp = true - } - } - - // Set up lifecycle-aware message monitoring - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - // Scroll to bottom when returning to the screen - if (event == Lifecycle.Event.ON_RESUME) { - if (messages.isNotEmpty()) { - coroutineScope.launch { - listState.scrollToItem(messages.size) - } - } - } - } - - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } - - // Handle back navigation requests - BackHandler { - viewModel.onBackPressed(onNavigateBack) - } - - Box( - modifier = Modifier.fillMaxSize() - ) { - ConversationMessageList( - messages = messages, - listState = listState, - ) - - if (showModelCard) { - selectedModel?.let { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .align(Alignment.TopCenter) - ) { - ModelCardWithSystemPrompt( - model = it, - loadingMetrics = loadingMetrics, - systemPrompt = systemPrompt, - isExpanded = isModelCardExpanded, - onExpanded = { isModelCardExpanded = !isModelCardExpanded } - ) - } - } - } - } - - // Unload confirmation dialog - ModelUnloadDialogHandler( - message = "Going back will unload the current model and clear the whole conversation.", - unloadModelState = unloadDialogState, - onUnloadConfirmed = { viewModel.onUnloadConfirmed(onNavigateBack) }, - onUnloadDismissed = { viewModel.onUnloadDismissed() }, - onNavigateBack = onNavigateBack, - ) -} - - -@Composable -fun ModelCardWithSystemPrompt( - model: ModelInfo, - loadingMetrics: ModelLoadingMetrics, - systemPrompt: String?, - isExpanded: Boolean = true, - onExpanded: ((Boolean) -> Unit)? = null, -) = ModelCardCoreExpandable( - model = model, - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - isExpanded = isExpanded, - onExpanded = onExpanded -) { - Spacer(modifier = Modifier.height(8.dp)) - - // Row 2: Context length, size label - ModelCardContentContextRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 3: Architecture, quantization, formatted size - ModelCardContentArchitectureRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 4: Model loading time - ModelCardContentField("Loading time", formatMilliSeconds(loadingMetrics.modelLoadingTimeMs)) - - if (!systemPrompt.isNullOrBlank()) { - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - - Text( - text = "System Prompt", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface, - ) - - Spacer(Modifier.height(6.dp)) - - Text( - text = systemPrompt, - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.ExtraLight, - fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Spacer(Modifier.height(6.dp)) - - loadingMetrics.systemPromptProcessingTimeMs?.let { - ModelCardContentField("Processing time", formatMilliSeconds(it)) - } - } -} - -@Composable -private fun ConversationMessageList( - messages: List, - listState: LazyListState, -) { - val context = LocalContext.current - val onInfoClick = { - Toast.makeText(context, "Please refer to this guide for more details on the metrics", Toast.LENGTH_SHORT).show() - val intent = Intent(Intent.ACTION_VIEW, "https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html".toUri()) - context.startActivity(intent) - } - - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), - verticalArrangement = Arrangement.Bottom, - ) { - items( - items = messages, - key = { "${it::class.simpleName}_${it.timestamp}" } - ) { message -> - when (message) { - is Message.User -> UserMessageBubble( - formattedTime = message.formattedTime, - content = message.content - ) - - is Message.Assistant.Ongoing -> AssistantMessageBubble( - formattedTime = message.formattedTime, - tokensCount = message.tokensCount, - content = message.content, - isThinking = message.content.isBlank(), - isGenerating = true, - metrics = null, - onInfoClick = onInfoClick, - ) - - is Message.Assistant.Stopped -> AssistantMessageBubble( - formattedTime = message.formattedTime, - tokensCount = message.metrics.tokensCount, - content = message.content, - isThinking = false, - isGenerating = false, - metrics = message.metrics, - onInfoClick = onInfoClick, - ) - } - } - - // Add extra space at the bottom for better UX and a scroll target - item(key = "bottom-spacer") { - Spacer(modifier = Modifier.height(36.dp)) - } - } -} - -@Composable -private fun UserMessageBubble(content: String, formattedTime: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalAlignment = Alignment.End - ) { - // Timestamp above bubble - Text( - text = formattedTime, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - modifier = Modifier.padding(bottom = 4.dp) - ) - - Row(modifier = Modifier.fillMaxWidth(0.9f)) { - Spacer(modifier = Modifier.weight(1f)) - - Card( - modifier = Modifier, - shape = RoundedCornerShape(16.dp, 2.dp, 16.dp, 16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Text( - text = content, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.padding(12.dp) - ) - } - } - } -} - -@Composable -private fun AssistantMessageBubble( - formattedTime: String, - tokensCount: Int, - content: String, - isThinking: Boolean, - isGenerating: Boolean, - metrics: TokenMetrics? = null, - onInfoClick: () -> Unit, -) { - val formattedTokensCount = when(tokensCount) { - 0 -> "" - 1 -> "1 token" - else -> "$tokensCount tokens" - } - - Column( - modifier = Modifier.fillMaxWidth(0.95f), - ) { - // AI Assistant Avatar + Timestamp + Token count - Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(36.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondary), - contentAlignment = Alignment.Center - ) { - Text( - text = "AI", - color = MaterialTheme.colorScheme.onSecondary, - style = MaterialTheme.typography.labelMedium - ) - } - - Column( - modifier = Modifier.padding(start = 12.dp), - ) { - Text( - text = formattedTime, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - modifier = Modifier.padding(bottom = 4.dp) - ) - - Text( - text = formattedTokensCount, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - ) - } - - } - - Card( - shape = RoundedCornerShape(2.dp, 16.dp, 16.dp, 16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - // Show actual content - Text( - modifier = Modifier.padding(12.dp), - text = content, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - - // Show metrics or generation status below the bubble - if (isGenerating) { - Row( - modifier = Modifier.height(20.dp).padding(top = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - PulsatingDots(small = true) - - Spacer(modifier = Modifier.width(4.dp)) - - Text( - text = if (isThinking) "Thinking" else "", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary - ) - } - } else { - // Show metrics when message is complete - metrics?.let { - ExpandableTokenMetricsBubble(metrics, onInfoClick) - } - } - - } -} - -@Composable -private fun PulsatingDots(small: Boolean = false) { - val transition = rememberInfiniteTransition(label = "dots") - - val animations = List(3) { index -> - transition.animateFloat( - initialValue = 0f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1000, - delayMillis = index * 300, - easing = LinearEasing - ), - repeatMode = RepeatMode.Reverse - ), - label = "dot-$index" - ) - } - - Row(verticalAlignment = Alignment.CenterVertically) { - animations.forEach { animation -> - Spacer(modifier = Modifier.width(2.dp)) - - Box( - modifier = Modifier - .size(if (small) 5.dp else 8.dp) - .clip(CircleShape) - .background( - color = MaterialTheme.colorScheme.primary.copy( - alpha = 0.3f + (animation.value * 0.7f) - ) - ) - ) - - Spacer(modifier = Modifier.width(2.dp)) - } - } -} - -@Composable -private fun ExpandableTokenMetricsBubble( - metrics: TokenMetrics, - onInfoClick: () -> Unit -) { - var showMetrics by remember { mutableStateOf(false) } - - Column(Modifier.fillMaxWidth()) { - FilterChip( - selected = showMetrics, - onClick = { showMetrics = !showMetrics }, - label = { - Text( - text = "${if (showMetrics) "Hide" else "Show"} stats", - modifier = Modifier.padding(start = 8.dp), - style = MaterialTheme.typography.labelMedium, - ) - }, - leadingIcon = { - Icon( - modifier = Modifier.size(16.dp), - imageVector = Icons.Default.Timer, - contentDescription = "${if (showMetrics) "Hide" else "Show"} token metrics of this assistant message" - ) - }, - ) - - if (showMetrics) { - Card( - shape = RoundedCornerShape(2.dp, 16.dp, 16.dp, 16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - val ttft = formatMilliSecondstructured(metrics.ttftMs) - TokenMetricSection("1st Token", ttft.value.toString(), ttft.unit.toEnglishName()) - - val tps = String.format(Locale.getDefault(), "%.2f", metrics.tpsMs) - TokenMetricSection("Decode speed", tps, "tokens/sec") - - val duration = formatMilliSecondstructured(metrics.duration) - TokenMetricSection("Duration", duration.value.toString(), duration.unit.toEnglishName()) - - IconButton(onClick = onInfoClick) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.AutoMirrored.Outlined.HelpOutline, - tint = MaterialTheme.colorScheme.onSecondaryContainer, - contentDescription = "Information on token metrics" - ) - } - } - } - } - } -} - -@Composable -private fun TokenMetricSection(metricName: String, metricValue: String, metricUnit: String) { - Column { - Text( - text = metricName, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Normal, - ) - - Text( - modifier = Modifier.padding(top = 2.dp), - text = metricValue, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Light, - fontStyle = FontStyle.Italic - ) - - Text( - modifier = Modifier.padding(top = 2.dp), - text = metricUnit, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - fontWeight = FontWeight.Light, - fontStyle = FontStyle.Italic - ) - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelDetailsScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelDetailsScreen.kt deleted file mode 100644 index 992447a0af..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelDetailsScreen.kt +++ /dev/null @@ -1,305 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AssistChip -import androidx.compose.material3.Card -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.arm.aichat.gguf.FileType -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.ui.components.ModelCardContentArchitectureRow -import com.arm.aiplayground.ui.components.ModelCardContentContextRow -import com.arm.aiplayground.ui.components.ModelCardContentTitleRow -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun ModelDetailsScreen( - model: ModelInfo, -) { - Column( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .verticalScroll(rememberScrollState()) - ) { - // Row 1: Model full name - ModelCardContentTitleRow(model) - - Spacer(modifier = Modifier.height(12.dp)) - - // Row 2: Context length, size label - ModelCardContentContextRow(model) - - Spacer(modifier = Modifier.height(8.dp)) - - // Row 3: Architecture, quantization, formatted size - ModelCardContentArchitectureRow(model) - - Spacer(modifier = Modifier.height(16.dp)) - - // Dates section - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Dates", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - val dateFormatter = remember { SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) } - - Column { - ListItem( - headlineContent = { Text("Added") }, - supportingContent = { - Text(dateFormatter.format(Date(model.dateAdded))) - } - ) - - model.dateLastUsed?.let { lastUsed -> - ListItem( - headlineContent = { Text("Last used") }, - supportingContent = { - Text(dateFormatter.format(Date(lastUsed))) - } - ) - } - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Metadata sections - only show if data exists - model.metadata.additional?.let { additional -> - if (additional.tags?.isNotEmpty() == true || additional.languages?.isNotEmpty() == true) { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Additional Information", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - additional.tags?.takeIf { it.isNotEmpty() }?.let { tags -> - Text( - text = "Tags", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium - ) - - Spacer(modifier = Modifier.height(4.dp)) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - tags.forEach { tag -> - AssistChip( - onClick = { /* No action */ }, - label = { Text(tag) } - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - } - - additional.languages?.takeIf { it.isNotEmpty() }?.let { languages -> - Text( - text = "Languages", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium - ) - - Spacer(modifier = Modifier.height(4.dp)) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - languages.forEach { language -> - AssistChip( - onClick = { /* No action */ }, - label = { Text(language) } - ) - } - } - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - } - } - - // Technical details section - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Technical Details", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Architecture details - model.metadata.architecture?.let { architecture -> - ListItem( - headlineContent = { Text("Architecture") }, - supportingContent = { Text(architecture.architecture ?: "Unknown") } - ) - - architecture.fileType?.let { - ListItem( - headlineContent = { Text("Quantization") }, - supportingContent = { Text(FileType.fromCode(it).label) } - ) - } - - architecture.vocabSize?.let { - ListItem( - headlineContent = { Text("Vocabulary Size") }, - supportingContent = { Text(it.toString()) } - ) - } - } - - // Context length - model.metadata.dimensions?.contextLength?.let { - ListItem( - headlineContent = { Text("Context Length") }, - supportingContent = { Text("$it tokens") } - ) - } - - // ROPE params if available - model.metadata.rope?.let { rope -> - ListItem( - headlineContent = { Text("RoPE Base") }, - supportingContent = { - rope.frequencyBase?.let { Text(it.toString()) } - } - ) - - ListItem( - headlineContent = { Text("RoPE Scaling") }, - supportingContent = { - if (rope.scalingType != null && rope.scalingFactor != null) { - Text("${rope.scalingType}: ${rope.scalingFactor}") - } else { - Text("None") - } - } - ) - } - - // File size - ListItem( - headlineContent = { Text("File Size") }, - supportingContent = { Text(model.formattedFileSize) } - ) - - // File path - ListItem( - headlineContent = { Text("File Path") }, - supportingContent = { - Text( - text = model.path, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - ) - } - } - - // Add author and attribution section if available - model.metadata.author?.let { author -> - if (author.author != null || author.organization != null || author.license != null) { - Spacer(modifier = Modifier.height(16.dp)) - - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Attribution", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - author.author?.let { - ListItem( - headlineContent = { Text("Author") }, - supportingContent = { Text(it) } - ) - } - - author.organization?.let { - ListItem( - headlineContent = { Text("Organization") }, - supportingContent = { Text(it) } - ) - } - - author.license?.let { - ListItem( - headlineContent = { Text("License") }, - supportingContent = { Text(it) } - ) - } - - author.url?.let { - ListItem( - headlineContent = { Text("URL") }, - supportingContent = { - Text( - text = it, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary - ) - } - ) - } - } - } - } - } - - Spacer(modifier = Modifier.height(24.dp)) - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelLoadingScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelLoadingScreen.kt deleted file mode 100644 index 974590eea4..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelLoadingScreen.kt +++ /dev/null @@ -1,575 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import android.content.Intent -import android.widget.Toast -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.selection.selectable -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import com.arm.aichat.InferenceEngine.State -import com.arm.aichat.UnsupportedArchitectureException -import com.arm.aiplayground.data.model.SystemPrompt -import com.arm.aiplayground.engine.ModelLoadingMetrics -import com.arm.aiplayground.ui.components.ModelCardCoreExpandable -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import com.arm.aiplayground.viewmodel.ModelLoadingViewModel - - -enum class Mode { - BENCHMARK, - CONVERSATION -} - -enum class SystemPromptTab(val label: String) { - PRESETS("Presets"), CUSTOM("Custom"), RECENTS("Recents") -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@Composable -fun ModelLoadingScreen( - onScaffoldEvent: (ScaffoldEvent) -> Unit, - onNavigateBack: () -> Unit, - onNavigateToBenchmark: (ModelLoadingMetrics) -> Unit, - onNavigateToConversation: (ModelLoadingMetrics) -> Unit, - viewModel: ModelLoadingViewModel, -) { - val context = LocalContext.current - - // View model states - val engineState by viewModel.engineState.collectAsState() - val selectedModel by viewModel.selectedModel.collectAsState() - val presetPrompts by viewModel.presetPrompts.collectAsState() - val recentPrompts by viewModel.recentPrompts.collectAsState() - - // UI states - var isModelCardExpanded by remember { mutableStateOf(true) } - var selectedMode by remember { mutableStateOf(null) } - var useSystemPrompt by remember { mutableStateOf(false) } - var showedSystemPromptWarning by remember { mutableStateOf(false) } - var selectedPrompt by remember { mutableStateOf(null) } - var selectedTab by remember { mutableStateOf(SystemPromptTab.PRESETS) } - var customPromptText by remember { mutableStateOf("") } - var expandedPromptId by remember { mutableStateOf(null) } - - // Automatically select first preset and expand it - LaunchedEffect(presetPrompts) { - if (presetPrompts.isNotEmpty() && selectedPrompt == null) { - val firstPreset = presetPrompts.first() - selectedPrompt = firstPreset - expandedPromptId = firstPreset.id - } - } - - // Determine if a system prompt is actually selected/entered when the switch is on - val hasActiveSystemPrompt = when { - !useSystemPrompt -> true // Not using system prompt, so this is fine - selectedTab == SystemPromptTab.CUSTOM -> customPromptText.isNotBlank() - else -> selectedPrompt != null - } - - // Check if we're in a loading state - val isLoading = engineState !is State.Initialized && engineState !is State.ModelReady - val exception = (engineState as? State.Error)?.exception - - // Handle back navigation requests - BackHandler { - viewModel.onBackPressed(onNavigateBack) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - // Selected model card - selectedModel?.let { model -> - ModelCardCoreExpandable( - model = model, - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - isExpanded = isModelCardExpanded, - onExpanded = { isModelCardExpanded = !isModelCardExpanded }, - ) - - Spacer(modifier = Modifier.height(16.dp)) - } - - // Benchmark card - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp) - .selectable( - selected = selectedMode == Mode.BENCHMARK, - onClick = { - selectedMode = Mode.BENCHMARK - useSystemPrompt = false - }, - enabled = !isLoading, - role = Role.RadioButton - ) - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedMode == Mode.BENCHMARK, - enabled = !isLoading, - onClick = null // handled by parent selectable - ) - Text( - text = "Benchmark", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(start = 8.dp) - ) - } - } - - // Conversation card with integrated system prompt picker & editor - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 4.dp) - .selectable( - selected = selectedMode == Mode.CONVERSATION, - onClick = { selectedMode = Mode.CONVERSATION }, - enabled = !isLoading, - role = Role.RadioButton - ) - // Only fill height if system prompt is active - .then(if (useSystemPrompt) Modifier.weight(1f) else Modifier) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - // Only fill height if system prompt is active - .then(if (useSystemPrompt) Modifier.fillMaxSize() else Modifier) - ) { - // Conversation option - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedMode == Mode.CONVERSATION, - enabled = !isLoading, - onClick = null // handled by parent selectable - ) - Text( - text = "Conversation", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(start = 8.dp) - ) - } - - // System prompt row with switch - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Use system prompt", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 32.dp) // Align with radio text - ) - - IconButton(onClick = { - Toast.makeText(context, "Please refer to this guide for more details on \"System Prompt\"", Toast.LENGTH_SHORT).show() - val intent = Intent(Intent.ACTION_VIEW, "https://docs.perplexity.ai/guides/prompt-guide#system-prompt".toUri()) - context.startActivity(intent) - }) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.AutoMirrored.Outlined.HelpOutline, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - contentDescription = "Information on system prompt" - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Switch( - checked = useSystemPrompt, - onCheckedChange = { - // First show a warning message if not yet - if (!showedSystemPromptWarning) { - onScaffoldEvent(ScaffoldEvent.ShowSnackbar( - message = "Model may not support system prompt!\nProceed with caution.", - duration = SnackbarDuration.Long, - )) - showedSystemPromptWarning = true - } - - // Then update states - useSystemPrompt = it - if (it && selectedMode != Mode.CONVERSATION) { - selectedMode = Mode.CONVERSATION - } - }, - enabled = !isLoading - ) - } - - // System prompt content (visible when switch is on) - AnimatedVisibility( - visible = useSystemPrompt && selectedMode == Mode.CONVERSATION, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxSize() - .padding(start = 48.dp, end = 16.dp) - ) { - HorizontalDivider( - modifier = Modifier - .padding(top = 4.dp, bottom = 8.dp) - ) - - SystemPromptTabSelector( - selectedTab = selectedTab, - onTabSelected = { selectedTab = it } - ) - - Spacer(modifier = Modifier.height(8.dp)) - - SystemPromptTabContent( - selectedTab = selectedTab, - presetPrompts = presetPrompts, - recentPrompts = recentPrompts, - customPromptText = customPromptText, - onCustomPromptChange = { - customPromptText = it - // Deselect any preset prompt if typing custom - if (it.isNotBlank()) { - selectedPrompt = null - } - }, - selectedPromptId = selectedPrompt?.id, - expandedPromptId = expandedPromptId, - onPromptSelected = { - selectedPrompt = it - expandedPromptId = it.id - }, - onExpandPrompt = { expandedPromptId = it } - ) - } - } - } - } - - // Flexible spacer when system prompt is not active - if (!useSystemPrompt) { - Spacer(modifier = Modifier.weight(1f)) - } else { - Spacer(modifier = Modifier.height(8.dp)) - } - - // Start button - Button( - onClick = { - when (selectedMode) { - Mode.BENCHMARK -> viewModel.onBenchmarkSelected(onNavigateToBenchmark) - - Mode.CONVERSATION -> { - val systemPrompt = if (useSystemPrompt) { - when (selectedTab) { - SystemPromptTab.PRESETS, SystemPromptTab.RECENTS -> - selectedPrompt?.let { prompt -> - // Save the prompt to recent prompts database - viewModel.savePromptToRecents(prompt) - prompt.content - } - - SystemPromptTab.CUSTOM -> - customPromptText.takeIf { it.isNotBlank() } - ?.also { promptText -> - // Save custom prompt to database - viewModel.saveCustomPromptToRecents(promptText) - } - } - } else null - viewModel.onConversationSelected(systemPrompt, onNavigateToConversation) - } - - null -> { /* No mode selected */ - } - } - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - colors = if (exception != null) - ButtonDefaults.buttonColors( - disabledContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f), - disabledContentColor = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.7f) - ) else ButtonDefaults.buttonColors(), - enabled = selectedMode != null && !isLoading && - (!useSystemPrompt || hasActiveSystemPrompt) - ) { - when { - exception != null -> { - val message = if (exception is UnsupportedArchitectureException) { - "Unsupported architecture: ${selectedModel?.metadata?.architecture?.architecture}" - } else { - exception.message ?: "Unknown error" - } - - Icon( - imageVector = Icons.Default.Error, - contentDescription = message, - tint = MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = message, - color = MaterialTheme.colorScheme.onErrorContainer - ) - } - - isLoading -> { - CircularProgressIndicator(modifier = Modifier - .height(24.dp) - .width(24.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = when (engineState) { - is State.Initializing, State.Initialized -> "Initializing..." - is State.LoadingModel -> "Loading model..." - is State.ProcessingSystemPrompt -> "Processing system prompt..." - else -> "Processing..." - }, - style = MaterialTheme.typography.titleMedium - ) - } - - else -> { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Run model ${selectedModel?.name} with $selectedMode" - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = "Start", style = MaterialTheme.typography.titleMedium) - } - } - } - } -} - -@Composable -private fun SystemPromptTabSelector( - selectedTab: SystemPromptTab, - onTabSelected: (SystemPromptTab) -> Unit -) { - SingleChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - SystemPromptTab.entries.forEachIndexed { index, tab -> - SegmentedButton( - selected = selectedTab == tab, - onClick = { onTabSelected(tab) }, - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = SystemPromptTab.entries.size - ), - icon = { - if (selectedTab == tab) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - }, - label = { Text(tab.label) } - ) - } - } -} - -@Composable -private fun SystemPromptTabContent( - selectedTab: SystemPromptTab, - presetPrompts: List, - recentPrompts: List, - customPromptText: String, - onCustomPromptChange: (String) -> Unit, - selectedPromptId: String?, - expandedPromptId: String?, - onPromptSelected: (SystemPrompt) -> Unit, - onExpandPrompt: (String) -> Unit -) { - when (selectedTab) { - SystemPromptTab.PRESETS, SystemPromptTab.RECENTS -> { - val prompts = if (selectedTab == SystemPromptTab.PRESETS) presetPrompts else recentPrompts - - if (prompts.isEmpty()) { - Text( - text = - if (selectedTab == SystemPromptTab.PRESETS) "No System Prompt presets available." - else "No recently used System Prompts found.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp) - ) - } else { - PromptList( - prompts = prompts, - selectedPromptId = selectedPromptId, - expandedPromptId = expandedPromptId, - onPromptSelected = onPromptSelected, - onExpandPrompt = onExpandPrompt - ) - } - } - - SystemPromptTab.CUSTOM -> { - OutlinedTextField( - value = customPromptText, - onValueChange = onCustomPromptChange, - modifier = Modifier - .fillMaxWidth() - .fillMaxSize(), - label = { Text("Customize your own system prompt here") }, - placeholder = { Text("You are a helpful assistant...") }, - minLines = 5 - ) - } - } -} - - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun PromptList( - prompts: List, - selectedPromptId: String?, - expandedPromptId: String?, - onPromptSelected: (SystemPrompt) -> Unit, - onExpandPrompt: (String) -> Unit -) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .fillMaxSize(), // Fill available space - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - items( - items = prompts, - key = { it.id } - ) { prompt -> - val isSelected = selectedPromptId == prompt.id - val isExpanded = expandedPromptId == prompt.id - - Column( - modifier = Modifier - .fillMaxWidth() - .animateItem() - .selectable( - selected = isSelected, - onClick = { - onPromptSelected(prompt) - onExpandPrompt(prompt.id) - } - ) - .padding(vertical = 8.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - RadioButton( - selected = isSelected, - onClick = null // Handled by selectable - ) - - Column( - modifier = Modifier - .weight(1f) - .padding(start = 8.dp) - ) { - Text( - text = prompt.title, - style = MaterialTheme.typography.titleSmall, - color = if (isSelected) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.onSurface - ) - - Text( - text = prompt.content, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = if (isExpanded) Int.MAX_VALUE else 2, - overflow = if (isExpanded) TextOverflow.Visible else TextOverflow.Ellipsis - ) - } - } - - if (prompt.id != prompts.last().id) { - HorizontalDivider( - modifier = Modifier.padding(top = 8.dp, start = 32.dp) - ) - } - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsBrowsingScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsBrowsingScreen.kt deleted file mode 100644 index 9cc22529df..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsBrowsingScreen.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.filled.FolderOpen -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.ui.components.InfoAction -import com.arm.aiplayground.ui.components.InfoView -import com.arm.aiplayground.ui.components.ModelCardFullExpandable -import com.arm.aiplayground.viewmodel.ModelsViewModel -import com.arm.aiplayground.viewmodel.PreselectedModelToRun - -@Composable -fun ModelsBrowsingScreen( - showChatTooltip: Boolean, - filteredModels: List?, - activeFiltersCount: Int, - preselection: PreselectedModelToRun?, - onManageModelsClicked: () -> Unit, - viewModel: ModelsViewModel, -) { - if (filteredModels == null) { - ModelsLoadingInProgressView() - } else if (filteredModels.isEmpty()) { - // Empty model prompt - val title = when (activeFiltersCount) { - 0 -> "No models installed yet" - 1 -> "No models match your filter" - else -> "No models match your filters" - } - val message = when (activeFiltersCount) { - 0 -> "Tap the button below to install your first Large Language Model!" - 1 -> "Try removing your filter to see more results" - else -> "Try removing some filters to see more results" - } - Box(modifier = Modifier.fillMaxSize()) { - InfoView( - modifier = Modifier.fillMaxSize(0.9f).align(Alignment.Center), - title = title, - icon = Icons.Default.FolderOpen, - message = message, - action = InfoAction( - label = "Get started", - icon = Icons.AutoMirrored.Default.ArrowForward, - onAction = onManageModelsClicked - ) - ) - } - } else { - // Model cards - LazyColumn( - Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), - ) { - items(items = filteredModels, key = { it.id }) { model -> - ModelCardFullExpandable( - model = model, - isSelected = if (model == preselection?.modelInfo) true else null, - onSelected = { selected -> - if (!selected) viewModel.resetPreselection() - }, - isExpanded = model == preselection?.modelInfo, - onExpanded = { expanded -> - viewModel.preselectModel(model, expanded) - } - ) - } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsManagementAndDeletingScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsManagementAndDeletingScreen.kt deleted file mode 100644 index 410c6dfaee..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsManagementAndDeletingScreen.kt +++ /dev/null @@ -1,770 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import android.content.Context -import android.content.Intent -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.automirrored.filled.Help -import androidx.compose.material.icons.automirrored.outlined.ContactSupport -import androidx.compose.material.icons.filled.Attribution -import androidx.compose.material.icons.filled.Celebration -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Today -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.core.net.toUri -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.data.source.remote.HuggingFaceModel -import com.arm.aiplayground.ui.components.InfoAction -import com.arm.aiplayground.ui.components.InfoAlertDialog -import com.arm.aiplayground.ui.components.InfoView -import com.arm.aiplayground.ui.components.ModelCardFullExpandable -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import com.arm.aiplayground.util.formatContextLength -import com.arm.aiplayground.util.formatFileByteSize -import com.arm.aiplayground.viewmodel.ModelManagementState -import com.arm.aiplayground.viewmodel.ModelManagementState.Deletion -import com.arm.aiplayground.viewmodel.ModelManagementState.Download -import com.arm.aiplayground.viewmodel.ModelManagementState.Importation -import com.arm.aiplayground.viewmodel.ModelScreenUiMode -import com.arm.aiplayground.viewmodel.ModelsManagementViewModel -import com.arm.aiplayground.viewmodel.ModelsViewModel -import java.text.SimpleDateFormat -import java.util.Locale - -/** - * Screen for managing LLM models (view, download, delete) - */ -@Composable -fun ModelsManagementAndDeletingScreen( - showModelImportTooltip: Boolean, - onFirstModelImportSuccess: (ModelInfo) -> Unit, - filteredModels: List?, - activeFiltersCount: Int, - isDeleting: Boolean, - onScaffoldEvent: (ScaffoldEvent) -> Unit, - modelsViewModel: ModelsViewModel, - managementViewModel: ModelsManagementViewModel, -) { - val context = LocalContext.current - - // Selection state - val selectedModels by managementViewModel.selectedModelsToDelete.collectAsState() - - // Model management state - val managementState by managementViewModel.managementState.collectAsState() - - // UI states - val expandedModels = remember { mutableStateMapOf() } - - BackHandler( - enabled = managementState is Importation.Importing - || managementState is Deletion.Deleting - ) { - /* Ignore back press while processing model management requests */ - } - - Box(modifier = Modifier.fillMaxSize()) { - if (filteredModels == null) { - ModelsLoadingInProgressView() - } else if (filteredModels.isEmpty()) { - // Prompt the user to import a model - val title = when (activeFiltersCount) { - 0 -> "Install your first model" - 1 -> "No models match\n the selected filter" - else -> "No models match\n the selected filters" - } - - val message = "If you have already obtained GGUF models on your computer, " + - "please transfer it onto your device, and then select \"Import a local GGUF model\".\n\n" + - "Otherwise, select \"Download from Hugging Face\" and pick one of the pre-selected models." - - InfoView( - modifier = Modifier.fillMaxSize(0.9f).align(Alignment.Center), - title = title, - icon = Icons.Default.Info, - message = message, - action = InfoAction( - label = "Learn more", - icon = Icons.AutoMirrored.Default.Help, - onAction = { - val url = "https://huggingface.co/docs/hub/en/gguf" - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - context.startActivity(intent) - } - ) - ) - } else { - // Model cards - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), - ) { - items(items = filteredModels, key = { it.id }) { model -> - val isSelected = - if (isDeleting) selectedModels.contains(model.id) else null - - ModelCardFullExpandable( - model = model, - isSelected = isSelected, - onSelected = { - if (isDeleting) { - managementViewModel.toggleModelSelectionById(filteredModels, model.id) - } - }, - isExpanded = expandedModels.contains(model.id), - onExpanded = { expanded -> - if (expanded) { - expandedModels.put(model.id, model) - } else { - expandedModels.remove(model.id) - } - } - ) - } - } - } - - // Model import progress overlay - when (val state = managementState) { - is Importation.Confirming -> { - ImportFromLocalFileDialog( - fileName = state.fileName, - fileSize = state.fileSize, - isImporting = false, - progress = 0.0f, - onConfirm = { - managementViewModel.importLocalModelFileConfirmed( - state.uri, state.fileName, state.fileSize - ) - }, - onCancel = { managementViewModel.resetManagementState() } - ) - } - - is Importation.Importing -> { - ImportFromLocalFileDialog( - fileName = state.fileName, - fileSize = state.fileSize, - isImporting = true, - isCancelling = state.isCancelling, - progress = state.progress, - onConfirm = {}, - onCancel = { managementViewModel.cancelOngoingLocalModelImport() }, - ) - } - - is Importation.Error -> { - ErrorDialog( - context = context, - title = "Import Failed", - message = state.message, - learnMoreUrl = state.learnMoreUrl, - onDismiss = { managementViewModel.resetManagementState() } - ) - } - - is Importation.Success -> { - if (showModelImportTooltip) { - FirstModelImportSuccessDialog { - onFirstModelImportSuccess(state.model) - managementViewModel.resetManagementState() - } - } else { - LaunchedEffect(state) { - onScaffoldEvent( - ScaffoldEvent.ShowSnackbar( - message = "Imported model: ${state.model.name}" - ) - ) - managementViewModel.resetManagementState() - } - } - } - - is Download.Querying -> { - ImportFromHuggingFaceDialog( - onCancel = { managementViewModel.resetManagementState() } - ) - } - - is Download.Ready -> { - ImportFromHuggingFaceDialog( - models = state.models, - onConfirm = { managementViewModel.downloadHuggingFaceModelConfirmed(it) }, - onCancel = { managementViewModel.resetManagementState() } - ) - } - - is Download.Dispatched -> { - DownloadHuggingFaceDispatchedDialog( - state.downloadInfo.modelId, - onConfirm = { managementViewModel.resetManagementState() } - ) - } - - is Download.Completed -> { - ImportFromLocalFileDialog( - fileName = state.fileName, - fileSize = state.fileSize, - isImporting = false, - progress = 0.0f, - onConfirm = { - managementViewModel.importLocalModelFileConfirmed( - state.uri, state.fileName, state.fileSize - ) - }, - onCancel = { managementViewModel.resetManagementState() } - ) - } - - is Download.Error -> { - ErrorDialog( - context = context, - title = "Download Failed", - message = state.message, - onDismiss = { managementViewModel.resetManagementState() } - ) - } - - is Deletion.Confirming -> { - BatchDeleteConfirmationDialog( - count = state.models.size, - onConfirm = { managementViewModel.deleteModels(state.models) }, - onDismiss = { managementViewModel.resetManagementState() }, - isDeleting = false - ) - } - - is Deletion.Deleting -> { - BatchDeleteConfirmationDialog( - count = state.models.size, - onConfirm = { /* No-op during processing */ }, - onDismiss = { /* No-op during processing */ }, - isDeleting = true - ) - } - - is Deletion.Error -> { - ErrorDialog( - context = context, - title = "Deletion Failed", - message = state.message, - onDismiss = { managementViewModel.resetManagementState() } - ) - } - - is Deletion.Success -> { - LaunchedEffect(state) { - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - - val count = state.models.size - onScaffoldEvent( - ScaffoldEvent.ShowSnackbar( - message = "Deleted $count ${if (count > 1) "models" else "model"}.", - withDismissAction = true, - duration = SnackbarDuration.Long, - ) - ) - } - } - - is ModelManagementState.Idle -> { /* Idle state, nothing to show */ } - } - } -} - -@Composable -private fun ImportFromLocalFileDialog( - fileName: String, - fileSize: Long, - isImporting: Boolean, - isCancelling: Boolean = false, - progress: Float, - onConfirm: () -> Unit, - onCancel: () -> Unit -) { - AlertDialog( - onDismissRequest = { - if (!isImporting) onCancel() - }, - properties = DialogProperties( - dismissOnBackPress = !isImporting, - dismissOnClickOutside = !isImporting - ), - title = { - Text(if (isImporting) "Importing Model" else "Confirm Import") - }, - text = { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Filename - Text( - text = fileName, - style = MaterialTheme.typography.bodyMedium, - fontStyle = FontStyle.Italic, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth() - ) - - if (isImporting) { - Spacer(modifier = Modifier.height(24.dp)) - - // Progress bar - LinearProgressIndicator( - progress = { progress }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Percentage text - Text( - text = "${(progress * 100).toInt()}%", - style = MaterialTheme.typography.bodyLarge - ) - } else { - // Show confirmation text - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Are you sure you want to import this model (${formatFileByteSize(fileSize)})? " + - "This may take up to several minutes.", - style = MaterialTheme.typography.bodyMedium, - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Additional information - if (isImporting) { - Text( - text = "This may take several minutes for large models", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } else if (isCancelling) { - Text( - text = "Cancelling ongoing import...", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error - ) - } - } - }, - confirmButton = { - // Only show confirm button in confirmation state - if (!isImporting) { - TextButton(onClick = onConfirm) { Text("Import") } - } - }, - dismissButton = { - if (!isImporting || (progress < 0.7f && !isCancelling)) { - TextButton(onClick = onCancel, enabled = !isCancelling) { - Text("Cancel") - } - } - } - ) -} - -@Composable -private fun ImportFromHuggingFaceDialog( - models: List? = null, - onConfirm: ((HuggingFaceModel) -> Unit)? = null, - onCancel: () -> Unit, -) { - val dateFormatter = remember { SimpleDateFormat("MMM, yyyy", Locale.getDefault()) } - - var selectedModel by remember { mutableStateOf(null) } - - AlertDialog( - onDismissRequest = {}, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = true - ), - title = { - Text(models?.let { "Fetched ${it.size} models" } ?: "Fetching models") - }, - text = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = models?.let { - "The Hugging Face models shown here have been pre-filtered to be moderately sized and correctly quantized.\n\n" + - "Please use responsibly. Arm® does not endorse or take responsibility for misuse or harmful use of these models.\n\n" + - "Select a model to download:" - } ?: "Searching on HuggingFace for open-source models free for downloading...", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Start, - ) - - if (models == null) { - Spacer(modifier = Modifier.height(24.dp)) - - CircularProgressIndicator( - modifier = Modifier.size(64.dp), - strokeWidth = 6.dp - ) - } else { - Spacer(modifier = Modifier.height(16.dp)) - - LazyColumn( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(models) { model -> - HuggingFaceModelListItem( - model = model, - isSelected = model._id == selectedModel?._id, - dateFormatter = dateFormatter, - onToggleSelect = { selected -> - selectedModel = if (selected) model else null - } - ) - } - } - } - } - }, - confirmButton = { - onConfirm?.let { onSelect -> - TextButton( - onClick = { selectedModel?.let { onSelect.invoke(it) } }, - enabled = selectedModel != null - ) { - Text("Download") - } - } - }, - dismissButton = { - TextButton( - onClick = onCancel - ) { - Text("Cancel") - } - } - ) -} - -@Composable -fun HuggingFaceModelListItem( - model: HuggingFaceModel, - isSelected: Boolean, - dateFormatter: SimpleDateFormat, - onToggleSelect: (Boolean) -> Unit, -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = when (isSelected) { - true -> CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - false -> CardDefaults.cardColors() - }, - onClick = { onToggleSelect(!isSelected) } - ) { - Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - Text( - modifier = Modifier.fillMaxWidth().basicMarquee(), - text = model.modelId.substringAfterLast("/"), - style = MaterialTheme.typography.bodyMedium, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, - ) - - Spacer(modifier = Modifier.size(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.fillMaxWidth(0.85f)) { - Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Attribution, - contentDescription = "Author", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = model.author, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.padding(start = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Favorite, - contentDescription = "Favorite count", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = formatContextLength(model.likes), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Row(modifier = Modifier.fillMaxWidth()) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Today, - contentDescription = "Author", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = dateFormatter.format(model.lastModified), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.padding(start = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Download, - contentDescription = "Download count", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - modifier = Modifier.padding(start = 4.dp), - text = formatContextLength(model.downloads), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - Box(modifier = Modifier.fillMaxSize()) { - Checkbox( - modifier = Modifier.align(Alignment.Center).size(32.dp) - .alpha(if (isSelected) 1f else 0f), - checked = isSelected, - onCheckedChange = null, // handled by parent selectable - ) - } - } - } - } -} - -@Composable -private fun DownloadHuggingFaceDispatchedDialog( - modelId: String, - onConfirm: () -> Unit, -) { - InfoAlertDialog( - title = "Download has started", - icon = Icons.Default.Download, - message = "Your Android system download manager has started downloading the model: $modelId.\n\n" - + "You can track its progress in your notification drawer.\n" - + "Feel free to stay on this screen, or come back to import it after complete.", - action = InfoAction( - icon = Icons.AutoMirrored.Default.ArrowForward, - label = "Okay", - onAction = onConfirm - ) - ) -} - -@Composable -private fun FirstModelImportSuccessDialog( - onConfirm: () -> Unit, -) { - InfoAlertDialog( - title = "Congratulations", - icon = Icons.Default.Celebration, - message = "You have just installed your first Large Language Model!", - action = InfoAction( - icon = Icons.AutoMirrored.Default.ArrowForward, - label = "Check it out", - onAction = onConfirm - ) - ) -} - -@Composable -private fun BatchDeleteConfirmationDialog( - count: Int, - onConfirm: () -> Unit, - onDismiss: () -> Unit, - isDeleting: Boolean = false -) { - AlertDialog( - // Prevent dismissal when deletion is in progress - onDismissRequest = { - if (!isDeleting) onDismiss() - }, - // Prevent dismissal via back button during deletion - properties = DialogProperties( - dismissOnBackPress = !isDeleting, - dismissOnClickOutside = !isDeleting - ), - title = { - Text("Confirm Deletion") - }, - text = { - Column { - Text( - "Are you sure you want to delete " - + "$count selected ${if (count == 1) "model" else "models"}? " - + "This operation cannot be undone." - ) - - if (isDeleting) { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Deleting models...") - } - } - } - }, - containerColor = MaterialTheme.colorScheme.errorContainer, - titleContentColor = MaterialTheme.colorScheme.onErrorContainer, - textContentColor = MaterialTheme.colorScheme.onErrorContainer, - confirmButton = { - TextButton( - onClick = onConfirm, - enabled = !isDeleting - ) { - Text( - text = "Delete", - color = if (!isDeleting) MaterialTheme.colorScheme.error - else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - ) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss, - enabled = !isDeleting - ) { - Text("Cancel") - } - } - ) -} - -@Composable -private fun ErrorDialog( - context: Context, - title: String, - message: String, - learnMoreUrl: String? = null, - onDismiss: () -> Unit -) { - val action = learnMoreUrl?.let { url -> - InfoAction( - label = "Learn more", - icon = Icons.AutoMirrored.Outlined.ContactSupport, - onAction = { - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - context.startActivity(intent) - } - ) - } - - InfoAlertDialog( - isCritical = true, - title = title, - allowDismiss = true, - onDismiss = onDismiss, - icon = Icons.Default.Error, - message = message, - action = action, - confirmButton = { - FilledTonalButton(onClick = onDismiss) { Text("Dismiss") } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsScreen.kt deleted file mode 100644 index 7addc34cad..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsScreen.kt +++ /dev/null @@ -1,200 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.ui.components.InfoView -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import com.arm.aiplayground.util.formatFileByteSize -import com.arm.aiplayground.viewmodel.ModelScreenUiMode -import com.arm.aiplayground.viewmodel.ModelsManagementViewModel -import com.arm.aiplayground.viewmodel.ModelsViewModel -import com.arm.aiplayground.viewmodel.PreselectedModelToRun.RamWarning - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModelsScreen( - showModelImportTooltip: Boolean, - onFirstModelImportSuccess: (ModelInfo) -> Unit, - showChatTooltip: Boolean, - onConfirmSelection: (ModelInfo, RamWarning) -> Unit, - onScaffoldEvent: (ScaffoldEvent) -> Unit, - modelsViewModel: ModelsViewModel, - managementViewModel: ModelsManagementViewModel, -) { - // Data - val filteredModels by modelsViewModel.filteredModels.collectAsState() - val preselection by modelsViewModel.preselectedModelToRun.collectAsState() - - // UI states: Filter - val activeFilters by modelsViewModel.activeFilters.collectAsState() - val activeFiltersCount by remember(activeFilters) { - derivedStateOf { activeFilters.count { it.value } } - } - - // UI states - val currentMode by modelsViewModel.modelScreenUiMode.collectAsState() - - // Handle back button press - BackHandler { - when (currentMode) { - ModelScreenUiMode.BROWSING -> { - if (preselection != null) { - modelsViewModel.resetPreselection() - } - } - ModelScreenUiMode.SEARCHING -> { - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - } - ModelScreenUiMode.MANAGING -> { - modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) - } - ModelScreenUiMode.DELETING -> { - managementViewModel.clearSelectedModelsToDelete() - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - } - } - } - - Box( - modifier = Modifier.fillMaxSize() - ) { - when (currentMode) { - ModelScreenUiMode.BROWSING -> - ModelsBrowsingScreen( - showChatTooltip = showChatTooltip, - filteredModels = filteredModels, - preselection = preselection, - onManageModelsClicked = { - managementViewModel.toggleImportMenu(true) - modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) - }, - activeFiltersCount = activeFiltersCount, - viewModel = modelsViewModel, - ) - ModelScreenUiMode.SEARCHING -> - ModelsSearchingScreen( - preselection = preselection, - viewModel = modelsViewModel - ) - ModelScreenUiMode.MANAGING, ModelScreenUiMode.DELETING -> - ModelsManagementAndDeletingScreen( - showModelImportTooltip = showModelImportTooltip, - onFirstModelImportSuccess = onFirstModelImportSuccess, - filteredModels = filteredModels, - isDeleting = currentMode == ModelScreenUiMode.DELETING, - onScaffoldEvent = onScaffoldEvent, - activeFiltersCount = activeFiltersCount, - modelsViewModel = modelsViewModel, - managementViewModel = managementViewModel, - ) - } - - // Show insufficient RAM warning - preselection?.let { - it.ramWarning?.let { warning -> - if (warning.showing) { - RamErrorDialog( - warning, - onDismiss = { modelsViewModel.dismissRamWarning() }, - onConfirm = { onConfirmSelection(it.modelInfo, warning) } - ) - } - } - } - } -} - -@Composable -fun ModelsLoadingInProgressView() { - InfoView( - modifier = Modifier.fillMaxSize(), - title = "Loading...", - icon = { - CircularProgressIndicator( - modifier = Modifier.size(64.dp), - strokeWidth = ProgressIndicatorDefaults.CircularStrokeWidth * 1.5f - ) - }, - message = "Searching for installed models on your device...", - ) -} - -@Composable -private fun RamErrorDialog( - ramError: RamWarning, - onDismiss: () -> Unit, - onConfirm: () -> Unit, -) { - val requiredRam = formatFileByteSize(ramError.requiredRam) - val availableRam = formatFileByteSize(ramError.availableRam) - - AlertDialog( - icon = { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = "Warning icon", - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.error - ) - }, - title = { - Text( - modifier = Modifier.padding(top = 16.dp), - text = "Insufficient RAM", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold - ) - }, - text = { - Text( - "You are trying to run a $requiredRam size model, " + - "but currently there's only $availableRam memory available!", - ) - }, - containerColor = MaterialTheme.colorScheme.errorContainer, - titleContentColor = MaterialTheme.colorScheme.onErrorContainer, - textContentColor = MaterialTheme.colorScheme.onErrorContainer, - dismissButton = { - TextButton( - onClick = onConfirm, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Text("Proceed") - } - }, - onDismissRequest = onDismiss, - confirmButton = { - FilledTonalButton(onClick = onDismiss) { - Text("Cancel") - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsSearchingScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsSearchingScreen.kt deleted file mode 100644 index 1a505248c9..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/ModelsSearchingScreen.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.input.clearText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.SearchOff -import androidx.compose.material3.Button -import androidx.compose.material3.DockedSearchBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.arm.aiplayground.ui.components.ModelCardFullExpandable -import com.arm.aiplayground.viewmodel.ModelScreenUiMode -import com.arm.aiplayground.viewmodel.ModelsViewModel -import com.arm.aiplayground.viewmodel.PreselectedModelToRun - -@ExperimentalMaterial3Api -@Composable -fun ModelsSearchingScreen( - preselection: PreselectedModelToRun?, - viewModel: ModelsViewModel, -) { - // Query states - val textFieldState = viewModel.searchFieldState - val searchQuery by remember(textFieldState) { - derivedStateOf { textFieldState.text.toString() } - } - val queryResults by viewModel.queryResults.collectAsState() - - // Local UI states - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - val toggleSearchFocusAndIme: (Boolean) -> Unit = { show -> - if (show) { - focusRequester.requestFocus() - keyboardController?.show() - } else { - focusRequester.freeFocus() - keyboardController?.hide() - } - } - - // TODO-han.yin: remove after validation -// LaunchedEffect (isSearchActive) { -// if (isSearchActive) { -// toggleSearchFocusAndIme(true) -// } -// } - - val handleExpanded: (Boolean) -> Unit = { expanded -> - viewModel.toggleMode( - if (expanded) ModelScreenUiMode.SEARCHING - else ModelScreenUiMode.BROWSING - ) - textFieldState.clearText() - } - - Box(modifier = Modifier.fillMaxSize()) { - DockedSearchBar( - modifier = Modifier.align(Alignment.TopCenter), - inputField = { - SearchBarDefaults.InputField( - modifier = Modifier.focusRequester(focusRequester), - query = textFieldState.text.toString(), - onQueryChange = { textFieldState.edit { replace(0, length, it) } }, - onSearch = {}, - expanded = true, - onExpandedChange = handleExpanded, - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) }, - placeholder = { Text("Type to search your models") } - ) - }, - expanded = true, - onExpandedChange = handleExpanded - ) { - queryResults?.let { results -> - if (results.isEmpty()) { - if (searchQuery.isNotBlank()) { - // If no results under current query, show "no results" message - EmptySearchResultsView( - onClearSearch = { - textFieldState.clearText() - toggleSearchFocusAndIme(true) - } - ) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), - ) { - items(items = results, key = { it.id }) { model -> - ModelCardFullExpandable( - model = model, - isSelected = if (model == preselection?.modelInfo) true else null, - onSelected = { selected -> - if (selected) { - toggleSearchFocusAndIme(false) - } else { - viewModel.resetPreselection() - toggleSearchFocusAndIme(true) - } - }, - isExpanded = model == preselection?.modelInfo, - onExpanded = { expanded -> - viewModel.preselectModel(model, expanded) - toggleSearchFocusAndIme(!expanded) - } - ) - } - } - } - } ?: ModelsLoadingInProgressView() - } - } -} - -@Composable -private fun EmptySearchResultsView( - onClearSearch: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Default.SearchOff, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "No matching models found", - style = MaterialTheme.typography.headlineSmall - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Try a different search term", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Button(onClick = onClearSearch) { - Text("Clear Search") - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/SettingsGeneralScreen.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/SettingsGeneralScreen.kt deleted file mode 100644 index 521b3dded2..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/screens/SettingsGeneralScreen.kt +++ /dev/null @@ -1,275 +0,0 @@ -package com.arm.aiplayground.ui.screens - -import android.content.Intent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import com.arm.aichat.ArmFeaturesMapper -import com.arm.aichat.ArmFeaturesMapper.DisplayItem -import com.arm.aiplayground.APP_NAME -import com.arm.aiplayground.BuildConfig -import com.arm.aiplayground.data.source.prefs.ColorThemeMode -import com.arm.aiplayground.data.source.prefs.DarkThemeMode -import com.arm.aiplayground.ui.components.ArmFeaturesVisualizer -import com.arm.aiplayground.viewmodel.SettingsViewModel -import kotlin.math.sqrt - -/** - * Screen for general app settings - */ -@Composable -fun SettingsGeneralScreen( - viewModel: SettingsViewModel, -) { - // Collect state from ViewModel - val isMonitoringEnabled by viewModel.isMonitoringEnabled.collectAsState() - val useFahrenheit by viewModel.useFahrenheitUnit.collectAsState() - val colorThemeMode by viewModel.colorThemeMode.collectAsState() - val darkThemeMode by viewModel.darkThemeMode.collectAsState() - val detectedTier = viewModel.detectedTier - - val supportedFeatures = remember(detectedTier) { - ArmFeaturesMapper.getFeatureDisplayData(detectedTier) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()) - ) { - SettingsCategory(title = "Performance Monitoring") { - SettingsSwitch( - title = "Enable Monitoring", - description = "Display memory, battery and temperature info", - checked = isMonitoringEnabled, - onCheckedChange = { viewModel.setMonitoringEnabled(it) } - ) - - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - - SettingsSwitch( - title = "Use Fahrenheit", - description = "Display temperature in Fahrenheit instead of Celsius", - checked = useFahrenheit, - onCheckedChange = { viewModel.setUseFahrenheitUnit(it) } - ) - } - - SettingsCategory(title = "Styling") { - // Color theme mode - Text( - text = "Color Theme", - style = MaterialTheme.typography.titleMedium - ) - - Text( - text = "Arm® or follow your system dynamic colors", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(16.dp)) - - SingleChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - ColorThemeMode.entries.forEachIndexed { index, mode -> - val weight = sqrt(sqrt(mode.label.length.toFloat())) - - SegmentedButton( - modifier = Modifier.weight(weight), - selected = colorThemeMode == mode, - onClick = { viewModel.setColorThemeMode(mode) }, - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = ColorThemeMode.entries.size - ) - ) { - Text(mode.label) - } - } - } - - HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) - - // Dark theme mode - Text( - text = "Dark Mode", - style = MaterialTheme.typography.titleMedium - ) - - Text( - text = "Follow system setting or override with your choice", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(16.dp)) - - SingleChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - DarkThemeMode.entries.forEachIndexed { index, mode -> - SegmentedButton( - selected = darkThemeMode == mode, - onClick = { viewModel.setDarkThemeMode(mode) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = DarkThemeMode.entries.size) - ) { - Text(mode.label) - } - } - } - } - - // ARM Features Visualizer with Tier Information description - detectedTier?.let { tier -> - SettingsCategory(title = "About your device") { - Text( - text = "AI Accelerated by Arm®", - style = MaterialTheme.typography.titleMedium - ) - - Text( - text = "Available hardware capabilities on your device are highlighted below:", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) - ) - - supportedFeatures?.let { - ArmFeaturesVisualizerClickable(supportedFeatures = it) - } - - Text( - text = "Tap a feature above to learn more about how it accelerates Generative AI!", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) - ) - } - } - - SettingsCategory(title = "About this app") { - Text( - text = APP_NAME, - style = MaterialTheme.typography.titleMedium - ) - - Text( - text = "Version ${BuildConfig.VERSION_NAME}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 4.dp) - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Run Large Language Models locally at your fingertips, harness the power of mobile AI with Arm®.", - style = MaterialTheme.typography.bodyMedium - ) - } - } -} - -@Composable -fun SettingsCategory( - title: String, - content: @Composable () -> Unit -) { - Column( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(bottom = 8.dp) - ) - - Card(modifier = Modifier.fillMaxWidth()) { - Column( modifier = Modifier.fillMaxWidth().padding(16.dp)) { - content() - } - } - - Spacer(modifier = Modifier.height(16.dp)) - } -} - -@Composable -fun SettingsSwitch( - title: String, - description: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Switch( - checked = checked, - onCheckedChange = onCheckedChange - ) - } -} - -/** - * Alternative version with clickable features that open ARM documentation. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ArmFeaturesVisualizerClickable( - supportedFeatures: List, -) { - val context = LocalContext.current - - ArmFeaturesVisualizer( - supportedFeatures = supportedFeatures, - onFeatureClick = { feature -> - // Open ARM documentation in browser - val intent = Intent(Intent.ACTION_VIEW, feature.armDocUrl.toUri()) - context.startActivity(intent) - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Color.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Color.kt deleted file mode 100644 index 0a5b43521d..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Color.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.arm.aiplayground.ui.theme - -import androidx.compose.ui.graphics.Color - - -// --- Light Theme Colors --- -val md_theme_light_primary = Color(0xFF0747C9) -val md_theme_light_onPrimary = Color(0xFFFCFCFC) -val md_theme_light_primaryContainer = Color(0xFFD0DEF6) -val md_theme_light_onPrimaryContainer = Color(0xFF1F2227) -val md_theme_light_inversePrimary = Color(0xFFD0DEF6) - -val md_theme_light_secondary = Color(0xFFE0BBFF) -val md_theme_light_onSecondary = Color(0xFF1F1A2A) -val md_theme_light_secondaryContainer = Color(0xFFF2DDFF) -val md_theme_light_onSecondaryContainer = Color(0xFF2A183F) - -val md_theme_light_tertiary = Color(0xFFFCFC00) -val md_theme_light_onTertiary = Color(0xFF1F2227) -val md_theme_light_tertiaryContainer = Color(0xFFEEEEAA) -val md_theme_light_onTertiaryContainer = Color(0xFF1F2227) - -val md_theme_light_background = Color(0xFFFCFCFC) -val md_theme_light_onBackground = Color(0xFF1F2227) -val md_theme_light_surface = Color(0xFFFCFCFC) -val md_theme_light_onSurface = Color(0xFF1F2227) -val md_theme_light_surfaceVariant = Color(0xFFD0DEF6) -val md_theme_light_onSurfaceVariant = Color(0xFF1F2227) -val md_theme_light_surfaceTint = Color(0xFFDFE1E7) -val md_theme_light_inverseSurface = Color(0xFF737A8B) -val md_theme_light_inverseOnSurface = Color(0xFFFCFCFC) - -val md_theme_light_error = Color(0xFF8F0D11) -val md_theme_light_onError = Color(0xFFFCFCFC) -val md_theme_light_errorContainer = Color(0xFFF5C8C3) -val md_theme_light_onErrorContainer = Color(0xFF1F2227) - -val md_theme_light_outline = Color(0xFF40454F) -val md_theme_light_outlineVariant = Color(0xFF8A92A5) -val md_theme_light_scrim = Color(0xFF000000) - - -val md_theme_light_surfaceBright = Color(0xFFD0DEF6) -val md_theme_light_surfaceContainerHighest = Color(0xFFDDE1E8) -val md_theme_light_surfaceContainerHigh = Color(0xFFE5E8ED) -val md_theme_light_surfaceContainer = Color(0xFFEBEDF1) -val md_theme_light_surfaceContainerLow = Color(0xFFF2F4F6) -val md_theme_light_surfaceContainerLowest = Color(0xFFF7F8F9) -val md_theme_light_surfaceDim = Color(0xFFB8BECB) - -val md_theme_light_primaryFixed = Color(0xFF0747C9) -val md_theme_light_primaryFixedDim = Color(0xFF04359B) -val md_theme_light_onPrimaryFixed = Color(0xFFFCFCFC) -val md_theme_light_onPrimaryFixedVariant = Color(0xFF2F333B) - -val md_theme_light_secondaryFixed = Color(0xFFFCFCFC) -val md_theme_light_secondaryFixedDim = Color(0xFFE9EBEF) -val md_theme_light_onSecondaryFixed = Color(0xFF1F2227) -val md_theme_light_onSecondaryFixedVariant = Color(0xFF2F333B) - -val md_theme_light_tertiaryFixed = Color(0xFF5613CD) -val md_theme_light_tertiaryFixedDim = Color(0xFF220857) -val md_theme_light_onTertiaryFixed = Color(0xFFFCFCFC) -val md_theme_light_onTertiaryFixedVariant = Color(0xFFDFE1E7) - - -// --- Dark Theme Colors --- -val md_theme_dark_primary = Color(0xFF0747C9) -val md_theme_dark_onPrimary = Color(0xFFFCFCFC) -val md_theme_dark_primaryContainer = Color(0xFF020D2B) -val md_theme_dark_onPrimaryContainer = Color(0xFFFCFCFC) -val md_theme_dark_inversePrimary = Color(0xFF020D2B) - -val md_theme_dark_secondary = Color(0xFF1F2227) -val md_theme_dark_onSecondary = Color(0xFFFCFCFC) -val md_theme_dark_secondaryContainer = Color(0xFF2F333B) -val md_theme_dark_onSecondaryContainer = Color(0xFFFCFCFC) - -val md_theme_dark_tertiary = Color(0xFF5613CD) -val md_theme_dark_onTertiary = Color(0xFFFCFCFC) -val md_theme_dark_tertiaryContainer = Color(0xFF0F0429) -val md_theme_dark_onTertiaryContainer = Color(0xFFFCFCFC) - -val md_theme_dark_background = Color(0xFF0F1216) -val md_theme_dark_onBackground = Color(0xFFFCFCFC) -val md_theme_dark_surface = Color(0xFF0F1216) -val md_theme_dark_onSurface = Color(0xFFFCFCFC) -val md_theme_dark_surfaceVariant = Color(0xFF020D2B) -val md_theme_dark_onSurfaceVariant = Color(0xFFFCFCFC) -val md_theme_dark_surfaceTint = Color(0xFF2F333B) -val md_theme_dark_inverseSurface = Color(0xFF8A92A5) -val md_theme_dark_inverseOnSurface = Color(0xFFFCFCFC) - -val md_theme_dark_error = Color(0xFF8F0D11) -val md_theme_dark_onError = Color(0xFFFCFCFC) -val md_theme_dark_errorContainer = Color(0xFF180203) -val md_theme_dark_onErrorContainer = Color(0xFFFCFCFC) - -val md_theme_dark_outline = Color(0xFFDFE1E7) -val md_theme_dark_outlineVariant = Color(0xFF9BA1B2) -val md_theme_dark_scrim = Color(0xFF000000) - -val md_theme_dark_surfaceBright = Color(0xFF020D2B) -val md_theme_dark_surfaceContainer = Color(0xFF2F333B) -val md_theme_dark_surfaceContainerLow = Color(0xFF1F2227) -val md_theme_dark_surfaceContainerHighest = Color(0xFF505562) -val md_theme_dark_surfaceContainerHigh = Color(0xFF40454F) -val md_theme_dark_surfaceContainerLowest = Color(0xFF0F1216) -val md_theme_dark_surfaceDim = Color(0xFF626977) - -val md_theme_dark_primaryFixed = Color(0xFF0747C9) -val md_theme_dark_primaryFixedDim = Color(0xFF04359B) -val md_theme_dark_onPrimaryFixed = Color(0xFFFCFCFC) -val md_theme_dark_onPrimaryFixedVariant = Color(0xFFDFE1E7) - -val md_theme_dark_secondaryFixed = Color(0xFF1F2227) -val md_theme_dark_secondaryFixedDim = Color(0xFF40454F) -val md_theme_dark_onSecondaryFixed = Color(0xFFFCFCFC) -val md_theme_dark_onSecondaryFixedVariant = Color(0xFFDFE1E7) - -val md_theme_dark_tertiaryFixed = Color(0xFF5613CD) -val md_theme_dark_tertiaryFixedDim = Color(0xFF220857) -val md_theme_dark_onTertiaryFixed = Color(0xFFFCFCFC) -val md_theme_dark_onTertiaryFixedVariant = Color(0xFFDFE1E7) diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Shape.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Shape.kt deleted file mode 100644 index f33a8486ed..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Shape.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.arm.aiplayground.ui.theme - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Shapes -import androidx.compose.ui.unit.dp - -val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(8.dp), - large = RoundedCornerShape(16.dp) -) - -// Additional custom shapes for specific components -val CardShape = RoundedCornerShape(12.dp) -val ButtonShape = RoundedCornerShape(8.dp) -val InputFieldShape = RoundedCornerShape(8.dp) diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Theme.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Theme.kt deleted file mode 100644 index 60e6408577..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Theme.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.arm.aiplayground.ui.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import com.arm.aiplayground.data.source.prefs.ColorThemeMode -import com.arm.aiplayground.data.source.prefs.DarkThemeMode - -// -------------------- Color Schemes -------------------- -internal val armLightColorScheme: ColorScheme = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - inversePrimary = md_theme_light_inversePrimary, - - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - surfaceTint = md_theme_light_surfaceTint, - inverseSurface = md_theme_light_inverseSurface, - inverseOnSurface = md_theme_light_inverseOnSurface, - - error = md_theme_light_error, - onError = md_theme_light_onError, - errorContainer = md_theme_light_errorContainer, - onErrorContainer = md_theme_light_onErrorContainer, - - outline = md_theme_light_outline, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim, - - surfaceBright = md_theme_light_surfaceBright, - surfaceContainer = md_theme_light_surfaceContainer, - surfaceContainerHigh = md_theme_light_surfaceContainerHigh, - surfaceContainerHighest = md_theme_light_surfaceContainerHighest, - surfaceContainerLow = md_theme_light_surfaceContainerLow, - surfaceContainerLowest = md_theme_light_surfaceContainerLowest, - surfaceDim = md_theme_light_surfaceDim, - - primaryFixed = md_theme_light_primaryFixed, - primaryFixedDim = md_theme_light_primaryFixedDim, - onPrimaryFixed = md_theme_light_onPrimaryFixed, - onPrimaryFixedVariant = md_theme_light_onPrimaryFixedVariant, - - secondaryFixed = md_theme_light_secondaryFixed, - secondaryFixedDim = md_theme_light_secondaryFixedDim, - onSecondaryFixed = md_theme_light_onSecondaryFixed, - onSecondaryFixedVariant = md_theme_light_onSecondaryFixedVariant, - - tertiaryFixed = md_theme_light_tertiaryFixed, - tertiaryFixedDim = md_theme_light_tertiaryFixedDim, - onTertiaryFixed = md_theme_light_onTertiaryFixed, - onTertiaryFixedVariant = md_theme_light_onTertiaryFixedVariant, -) - -internal val armDarkColorScheme: ColorScheme = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - inversePrimary = md_theme_dark_inversePrimary, - - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - surfaceTint = md_theme_dark_surfaceTint, - inverseSurface = md_theme_dark_inverseSurface, - inverseOnSurface = md_theme_dark_inverseOnSurface, - - error = md_theme_dark_error, - onError = md_theme_dark_onError, - errorContainer = md_theme_dark_errorContainer, - onErrorContainer = md_theme_dark_onErrorContainer, - - outline = md_theme_dark_outline, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim, - - surfaceBright = md_theme_dark_surfaceBright, - surfaceContainer = md_theme_dark_surfaceContainer, - surfaceContainerHigh = md_theme_dark_surfaceContainerHigh, - surfaceContainerHighest = md_theme_dark_surfaceContainerHighest, - surfaceContainerLow = md_theme_dark_surfaceContainerLow, - surfaceContainerLowest = md_theme_dark_surfaceContainerLowest, - surfaceDim = md_theme_dark_surfaceDim, - - primaryFixed = md_theme_dark_primaryFixed, - primaryFixedDim = md_theme_dark_primaryFixedDim, - onPrimaryFixed = md_theme_dark_onPrimaryFixed, - onPrimaryFixedVariant = md_theme_dark_onPrimaryFixedVariant, - - secondaryFixed = md_theme_dark_secondaryFixed, - secondaryFixedDim = md_theme_dark_secondaryFixedDim, - onSecondaryFixed = md_theme_dark_onSecondaryFixed, - onSecondaryFixedVariant = md_theme_dark_onSecondaryFixedVariant, - - tertiaryFixed = md_theme_dark_tertiaryFixed, - tertiaryFixedDim = md_theme_dark_tertiaryFixedDim, - onTertiaryFixed = md_theme_dark_onTertiaryFixed, - onTertiaryFixedVariant = md_theme_dark_onTertiaryFixedVariant, -) - -@Composable -fun isDarkTheme(darkThemeMode: DarkThemeMode) = - when (darkThemeMode) { - DarkThemeMode.AUTO -> isSystemInDarkTheme() - DarkThemeMode.LIGHT -> false - DarkThemeMode.DARK -> true - } - -@Composable -fun LlamaTheme( - colorThemeMode: ColorThemeMode, - isDarkTheme: Boolean, - content: @Composable () -> Unit -) { - val context = LocalContext.current - val colorScheme = when(colorThemeMode) { - ColorThemeMode.ARM -> - if (isDarkTheme) armDarkColorScheme else armLightColorScheme - ColorThemeMode.MATERIAL -> - if (isDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - shapes = Shapes, - content = content - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Type.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Type.kt deleted file mode 100644 index a15959c6a3..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/ui/theme/Type.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.arm.aiplayground.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - displayLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp - ), - displayMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - headlineLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - bodyMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - labelLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) -) - -// Additional specific text styles -val MonospacedTextStyle = TextStyle( - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 20.sp, - letterSpacing = 0.sp -) diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/FileUtils.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/FileUtils.kt deleted file mode 100644 index 75d0c1d4c7..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/FileUtils.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.arm.aiplayground.util - -import android.content.Context -import android.net.Uri -import android.provider.OpenableColumns -import kotlinx.coroutines.yield -import java.io.BufferedInputStream -import java.io.BufferedOutputStream -import java.io.InputStream -import java.io.OutputStream -import java.nio.ByteBuffer -import java.nio.channels.Channels -import java.nio.channels.ReadableByteChannel -import java.nio.channels.WritableByteChannel - -/** - * Gets the file name from a content URI - */ -fun getFileNameFromUri(context: Context, uri: Uri): String? = - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { nameIndex -> - if (nameIndex != -1) cursor.getString(nameIndex) else null - } - } else { - null - } - } ?: uri.lastPathSegment - -/** - * Gets the file size from a content URI - */ -fun getFileSizeFromUri(context: Context, uri: Uri): Long? = - context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - cursor.getColumnIndex(OpenableColumns.SIZE).let { sizeIndex -> - if (sizeIndex != -1) cursor.getLong(sizeIndex) else null - } - } else { - null - } - } - - -suspend fun copyWithChannels( - input: InputStream, - output: OutputStream, - totalSize: Long, - bufferSize: Int, - yieldSize: Int, - onProgress: (suspend (Float) -> Unit)? -) { - val inChannel: ReadableByteChannel = Channels.newChannel(input) - val outChannel: WritableByteChannel = Channels.newChannel(output) - - val buffer = ByteBuffer.allocateDirect(bufferSize) - var totalBytesRead = 0L - var yieldCounter = 0L - - while (inChannel.read(buffer) != -1) { - buffer.flip() - while (buffer.hasRemaining()) { - outChannel.write(buffer) - } - - val bytesRead = buffer.position() - totalBytesRead += bytesRead - yieldCounter += bytesRead - buffer.clear() - - // Report progress - onProgress?.invoke(totalBytesRead.toFloat() / totalSize) - - if (yieldCounter >= yieldSize) { - yield() - yieldCounter = 0L - } - } - - outChannel.close() - inChannel.close() -} - -suspend fun copyWithBuffer( - input: InputStream, - output: OutputStream, - totalSize: Long, - bufferSize: Int, - yieldSize: Int, - onProgress: (suspend (Float) -> Unit)? -) { - val bufferedInput = BufferedInputStream(input, bufferSize) - val bufferedOutput = BufferedOutputStream(output, bufferSize) - val buffer = ByteArray(bufferSize) - - var bytesRead: Int - var totalBytesRead = 0L - - while (input.read(buffer).also { bytesRead = it } != -1) { - output.write(buffer, 0, bytesRead) - totalBytesRead += bytesRead - - // Report progress - onProgress?.invoke(totalBytesRead.toFloat() / totalSize) - - // Yield less frequently with larger buffers - if (totalBytesRead % (yieldSize) == 0L) { // Every 64MB - yield() - } - } - - bufferedOutput.close() - bufferedInput.close() -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/FormatUtils.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/FormatUtils.kt deleted file mode 100644 index 840bdf9adc..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/FormatUtils.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.arm.aiplayground.util - -import java.util.concurrent.TimeUnit -import java.util.Locale -import kotlin.math.round - -/** - * Maps [TimeUnit] to English names, with plural form support - */ -fun TimeUnit.toEnglishName(plural: Boolean = false): String = when (this) { - TimeUnit.NANOSECONDS -> if (plural) "nanoseconds" else "nanosecond" - TimeUnit.MICROSECONDS -> if (plural) "microseconds" else "microsecond" - TimeUnit.MILLISECONDS -> if (plural) "milliseconds" else "millisecond" - TimeUnit.SECONDS -> if (plural) "seconds" else "second" - TimeUnit.MINUTES -> if (plural) "minutes" else "minute" - TimeUnit.HOURS -> if (plural) "hours" else "hour" - TimeUnit.DAYS -> if (plural) "days" else "day" -} - -/** - * Formats milliseconds into a human-readable time string - * e.g., 2300ms -> "2.3 sec" - */ -fun formatMilliSeconds(millis: Long): String { - val seconds = millis / 1000.0 - return if (seconds < 1.0) { - "${(seconds * 1000).toInt()} ms" - } else if (seconds < 60.0) { - "%.1f sec".format(seconds) - } else { - val minutes = seconds / 60.0 - "%.1f min".format(minutes) - } -} - -data class DurationValue( - val value: Double, - val unit: TimeUnit -) - -/** - * Converts milliseconds into a structured DurationValue. - * - * Rules: - * - < 100 seconds -> show in SECONDS - * - < 100 minutes -> show in MINUTES - * - < 100 hours -> show in HOURS - */ -fun formatMilliSecondstructured(millis: Long): DurationValue { - val seconds = millis / 1000.0 - return when { - seconds < 100 -> DurationValue(round2(seconds), TimeUnit.SECONDS) - seconds < 100 * 60 -> DurationValue(round2(seconds / 60.0), TimeUnit.MINUTES) - else -> DurationValue(round2(seconds / 3600.0), TimeUnit.HOURS) - } -} - -private fun round2(v: Double): Double = round(v * 100) / 100 - - -/** - * Convert bytes into human readable sizes - */ -fun formatFileByteSize(sizeInBytes: Long) = when { - sizeInBytes >= 1_000_000_000 -> { - val sizeInGb = sizeInBytes / 1_000_000_000.0 - String.format(Locale.getDefault(), "%.1f GB", sizeInGb) - } - sizeInBytes >= 1_000_000 -> { - val sizeInMb = sizeInBytes / 1_000_000.0 - String.format(Locale.getDefault(), "%.0f MB", sizeInMb) - } - else -> { - val sizeInKb = sizeInBytes / 1_000.0 - String.format(Locale.getDefault(), "%.0f KB", sizeInKb) - } -} - -/** - * Formats numbers to human-readable form (K, M) - */ -fun formatContextLength(contextLength: Int): String { - return when { - contextLength >= 1_000_000 -> String.format(Locale.getDefault(), "%.1fM", contextLength / 1_000_000.0) - contextLength >= 1_000 -> String.format(Locale.getDefault(), "%.0fK", contextLength / 1_000.0) - else -> contextLength.toString() - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/LocaleUtils.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/LocaleUtils.kt deleted file mode 100644 index 2bbffacd08..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/LocaleUtils.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.arm.aiplayground.util - - -/** - * Maps ISO 639-1 language codes into ISO 3166-1 alpha-2 country codes, and then map to Emoji. - * - */ -fun languageCodeToFlagEmoji(languageCode: String): String? { - SPECIAL_LANGUAGES[languageCode]?.let { return it } - - val countryCode = LANGUAGE_TO_COUNTRY[languageCode.lowercase()] ?: return null - - return countryCodeToFlagEmoji(countryCode) -} - -/** - * Formats ISO 3166-1 alpha-2 country code into corresponding Emoji. - */ -private fun countryCodeToFlagEmoji(countryCode: String): String? { - if (countryCode.length != 2) return null - - // Convert each character to a Regional Indicator Symbol - val firstChar = Character.codePointAt(countryCode.uppercase(), 0) - 'A'.code + 0x1F1E6 - val secondChar = Character.codePointAt(countryCode.uppercase(), 1) - 'A'.code + 0x1F1E6 - - return String(Character.toChars(firstChar)) + String(Character.toChars(secondChar)) -} - -private val SPECIAL_LANGUAGES = mapOf( - "multi" to "🌐", - "multilingual" to "🌐", - "multi-lingual" to "🌐", -) - -private val LANGUAGE_TO_COUNTRY by lazy { - mapOf( - "af" to "ZA", - "am" to "ET", - "ar" to "SA", - "bg" to "BG", - "bn" to "BD", - "cs" to "CZ", - "da" to "DK", - "de" to "DE", - "el" to "GR", - "en" to "US", - "es" to "ES", - "et" to "EE", - "fa" to "IR", - "fi" to "FI", - "fil" to "PH", - "fr" to "FR", - "he" to "IL", - "hi" to "IN", - "hr" to "HR", - "hu" to "HU", - "id" to "ID", - "it" to "IT", - "ja" to "JP", - "kn" to "IN", - "ko" to "KR", - "lt" to "LT", - "lv" to "LV", - "ml" to "IN", - "mr" to "IN", - "ms" to "MY", - "nl" to "NL", - "no" to "NO", - "pa" to "IN", - "pl" to "PL", - "pt" to "PT", - "ro" to "RO", - "ru" to "RU", - "sk" to "SK", - "sl" to "SI", - "sr" to "RS", - "sv" to "SE", - "sw" to "KE", - "ta" to "LK", - "te" to "IN", - "th" to "TH", - "tr" to "TR", - "uk" to "UA", - "ur" to "PK", - "vi" to "VN", - "zh" to "CN", - "zu" to "ZA", - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/NaiveMetadataExtractor.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/NaiveMetadataExtractor.kt deleted file mode 100644 index 11546f563a..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/NaiveMetadataExtractor.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.arm.aiplayground.util - -import java.util.Locale - - -@Deprecated("Use GgufMetadataReader instead!") -class NaiveMetadataExtractor private constructor() { - /** - * Try to extract parameters by looking for patterns like 7B, 13B, etc. - */ - fun extractParametersFromFilename(filename: String): String? = - Regex("([0-9]+(\\.[0-9]+)?)[bB]").find(filename)?.value?.uppercase() - - /** - * Try to extract quantization by looking for patterns like Q4_0, Q5_K_M, etc. - */ - fun extractQuantizationFromFilename(filename: String) = - listOf( - Regex("[qQ][0-9]_[0-9]"), - Regex("[qQ][0-9]_[kK]_[mM]"), - Regex("[qQ][0-9]_[kK]"), - Regex("[qQ][0-9][fF](16|32)") - ).firstNotNullOfOrNull { - it.find(filename)?.value?.uppercase() - } - - /** - * Try to extract model type (Llama, Mistral, etc.) - */ - fun extractModelTypeFromFilename(filename: String): String? = - filename.lowercase().let { lowerFilename -> - listOf("llama", "mistral", "phi", "qwen", "falcon", "mpt") - .firstNotNullOfOrNull { type -> - if (lowerFilename.contains(type)) { - type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - } - } else { null } - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/TableUtils.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/TableUtils.kt deleted file mode 100644 index 80e49a11d3..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/util/TableUtils.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.arm.aiplayground.util - - -/** - * A basic table data holder separating rows and columns - */ -data class TableData( - val headers: List, - val rows: List> -) { - val columnCount: Int get() = headers.size - val rowCount: Int get() = rows.size - - /** - * Generate a copy of the original table with only the [keep] columns - */ - fun filterColumns(keep: Set): TableData = - headers.mapIndexedNotNull { index, name -> - if (name in keep) index else null - }.let { keepIndices -> - val newHeaders = keepIndices.map { headers[it] } - val newRows = rows.map { row -> keepIndices.map { row.getOrElse(it) { "" } } } - TableData(newHeaders, newRows) - } - - /** - * Obtain the data in the specified column - */ - fun getColumn(name: String): List { - val index = headers.indexOf(name) - if (index == -1) return emptyList() - return rows.mapNotNull { it.getOrNull(index) } - } -} - -/** - * Formats llama-bench's markdown output into structured [MarkdownTableData] - */ -fun parseMarkdownTable(markdown: String): TableData { - val lines = markdown.trim().lines().filter { it.startsWith("|") } - if (lines.size < 2) return TableData(emptyList(), emptyList()) - - val headers = lines[0].split("|").map { it.trim() }.filter { it.isNotEmpty() } - val rows = lines.drop(2).map { line -> - line.split("|").map { it.trim() }.filter { it.isNotEmpty() } - } - - return TableData(headers, rows) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/BenchmarkViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/BenchmarkViewModel.kt deleted file mode 100644 index 56fd87d6de..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/BenchmarkViewModel.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.lifecycle.viewModelScope -import com.arm.aichat.isUninterruptible -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.engine.BenchmarkService -import com.arm.aiplayground.ui.scaffold.ScaffoldEvent -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.zip -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class BenchmarkViewModel @Inject constructor( - private val benchmarkService: BenchmarkService -) : ModelUnloadingViewModel(benchmarkService) { - - // Data - val selectedModel: StateFlow = benchmarkService.currentSelectedModel - - private val _benchmarkDuration = MutableSharedFlow() - - private val _benchmarkResults = MutableStateFlow>(emptyList()) - val benchmarkResults: StateFlow> = _benchmarkResults.asStateFlow() - - // UI state: Model card - private val _showModelCard = MutableStateFlow(false) - val showModelCard = _showModelCard.asStateFlow() - - fun toggleModelCard(show: Boolean) { - _showModelCard.value = show - } - - // UI state: Share FAB - private val _showShareFab = MutableStateFlow(false) - val showShareFab = _showShareFab.asStateFlow() - - init { - viewModelScope.launch { - benchmarkService.benchmarkResults - .filterNotNull() - .zip(_benchmarkDuration) { result, duration -> - _benchmarkResults.update { oldResults -> - oldResults.toMutableList().apply { - add(BenchmarkResult(result, duration)) - } - } - }.collect() - } - } - - /** - * Run benchmark with specified parameters - */ - fun runBenchmark(pp: Int = 512, tg: Int = 128, pl: Int = 1, nr: Int = 3): Boolean { - if (engineState.value.isUninterruptible) { - return false - } - - viewModelScope.launch { - _showShareFab.value = false - val benchmarkStartTs = System.currentTimeMillis() - benchmarkService.benchmark(pp, tg, pl, nr) - val benchmarkEndTs = System.currentTimeMillis() - _benchmarkDuration.emit(benchmarkEndTs - benchmarkStartTs) - _showShareFab.value = true - } - return true - } - - override suspend fun performCleanup() { clearResults(null) } - - fun clearResults(onScaffoldEvent: ((ScaffoldEvent) -> Unit)?) = - if (engineState.value.isUninterruptible) { - false - } else { - _benchmarkResults.value = emptyList() - _showShareFab.value = false - onScaffoldEvent?.invoke(ScaffoldEvent.ShowSnackbar( - message = "All benchmark results cleared." - )) - true - } - - /** - * Rerun the benchmark - */ - fun rerunBenchmark(onScaffoldEvent: (ScaffoldEvent) -> Unit) { - if (engineState.value.isUninterruptible) { - onScaffoldEvent(ScaffoldEvent.ShowSnackbar( - message = "Benchmark already in progress!\n" + - "Please wait for the current run to complete." - )) - } else { - runBenchmark() - } - } - - fun shareResult(onScaffoldEvent: (ScaffoldEvent) -> Unit) { - _benchmarkResults.value.lastOrNull()?.let{ - onScaffoldEvent(ScaffoldEvent.ShareText(it.text)) - } - } -} - -data class BenchmarkResult( - val text: String, - val duration: Long -) diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ConversationViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ConversationViewModel.kt deleted file mode 100644 index b9d4794d17..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ConversationViewModel.kt +++ /dev/null @@ -1,206 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.lifecycle.viewModelScope -import com.arm.aiplayground.engine.ConversationService -import com.arm.aiplayground.engine.GenerationUpdate -import com.arm.aiplayground.engine.TokenMetrics -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.launch -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import javax.inject.Inject - - -@HiltViewModel -class ConversationViewModel @Inject constructor( - private val conversationService: ConversationService -) : ModelUnloadingViewModel(conversationService) { - // Data - val selectedModel = conversationService.currentSelectedModel - val systemPrompt = conversationService.systemPrompt - - // UI state: Model card - private val _showModelCard = MutableStateFlow(false) - val showModelCard = _showModelCard.asStateFlow() - - fun toggleModelCard(show: Boolean) { - _showModelCard.value = show - } - - // UI state: conversation messages - private val _messages = MutableStateFlow>(emptyList()) - val messages: StateFlow> = _messages.asStateFlow() - - // UI state: Input text field - val inputFieldState = TextFieldState() - - // Ongoing coroutine jobs - private var tokenCollectionJob: Job? = null - - /** - * Send a message with the provided content - */ - fun sendMessage() { - val content = inputFieldState.text.toString() - if (content.isBlank()) return - - // Cancel ongoing collection - stopGeneration() - - // Add user message - val userMessage = Message.User( - content = content, - timestamp = System.currentTimeMillis() - ) - _messages.value += userMessage - - // Add placeholder for assistant response - val assistantMessage = Message.Assistant.Ongoing( - content = "", - timestamp = System.currentTimeMillis(), - tokensCount = 0, - ) - _messages.value += assistantMessage - - // Clear input field - inputFieldState.clearText() - - // Collect response - tokenCollectionJob = viewModelScope.launch { - try { - conversationService.generateResponse(content) - .onCompletion { tokenCollectionJob = null } - .collect(::updateAssistantMessage) - - } catch (_: CancellationException) { - handleCancellation() - tokenCollectionJob = null - - } catch (e: Exception) { - handleResponseError(e) - tokenCollectionJob = null - } - } - } - - /** - * Stop ongoing generation - */ - fun stopGeneration() = - tokenCollectionJob?.let { job -> - // handled by the catch blocks - if (job.isActive) { job.cancel() } - } - - /** - * Handle the case when generation is explicitly cancelled by adding a stopping suffix - */ - private fun handleCancellation() = - _messages.value.toMutableList().apply { - (removeLastOrNull() as? Message.Assistant.Stopped)?.let { - add(it.copy(content = it.content + SUFFIX_GENERATION_STOPPED)) - _messages.value = toList() - } - } - - /** - * Handle response error by appending an error suffix - */ - private fun handleResponseError(e: Exception) = - _messages.value.toMutableList().apply { - (removeLastOrNull() as? Message.Assistant.Stopped)?.let { - add(it.copy(content = it.content + SUFFIX_GENERATION_ERROR.format(e.message))) - _messages.value = this.toList() - } - } - - /** - * Handle updating the assistant message - */ - private fun updateAssistantMessage(update: GenerationUpdate) = - _messages.value.toMutableList().apply { - (removeLastOrNull() as? Message.Assistant.Ongoing)?.let { - if (update.metrics != null) { - // Finalized message (partial or complete) with metrics - add(Message.Assistant.Stopped( - content = update.text, - timestamp = it.timestamp, - metrics = update.metrics - )) - } else if (!update.isComplete) { - // Ongoing message update - add(Message.Assistant.Ongoing( - content = update.text, - timestamp = it.timestamp, - tokensCount = it.tokensCount + 1, - )) - } - _messages.value = toList() - } - } - - override suspend fun performCleanup() = clearConversation() - - /** - * Stop ongoing generation if any, then clean up all messages in the current conversation - */ - fun clearConversation() { - stopGeneration() - _messages.value = emptyList() - } - - override fun onCleared() { - stopGeneration() - super.onCleared() - } - - companion object { - private const val SUFFIX_GENERATION_STOPPED = " [Generation stopped]" - private const val SUFFIX_GENERATION_ERROR = " [Error: %s]" - } -} - - -/** - * Sealed class representing messages in a conversation. - */ -sealed class Message { - abstract val timestamp: Long - abstract val content: String - - val formattedTime: String - get() = datetimeFormatter.format(Date(timestamp)) - - data class User( - override val timestamp: Long, - override val content: String - ) : Message() - - sealed class Assistant : Message() { - data class Ongoing( - override val timestamp: Long, - override val content: String, - val tokensCount: Int, - ) : Assistant() - - data class Stopped( - override val timestamp: Long, - override val content: String, - val metrics: TokenMetrics - ) : Assistant() - } - - companion object { - private val datetimeFormatter by lazy { SimpleDateFormat("h:mm a", Locale.getDefault()) } - } -} - diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/MainViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/MainViewModel.kt deleted file mode 100644 index ab59483703..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/MainViewModel.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.arm.aiplayground.data.source.prefs.AppPreferences -import com.arm.aiplayground.engine.InferenceService -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -/** - * Main ViewModel that expose the core states of [InferenceService] and [AppPreferences] - */ -class MainViewModel @Inject constructor ( - private val appPreferences: AppPreferences, - private val inferenceService: InferenceService, -) : ViewModel() { - - val engineState = inferenceService.engineState - - // App preferences - private val _showModelImportTooltip = MutableStateFlow(true) - val showModelImportTooltip: StateFlow = _showModelImportTooltip.asStateFlow() - - private val _showChatTooltip = MutableStateFlow(true) - val showChatTooltip: StateFlow = _showChatTooltip.asStateFlow() - - private val _showModelManagementTooltip = MutableStateFlow(true) - val showModelManagementTooltip: StateFlow = _showModelManagementTooltip.asStateFlow() - - - /** - * Unload the current model and release the resources - */ - suspend fun unloadModel() = inferenceService.unloadModel() - - init { - viewModelScope.launch { - launch { - appPreferences.userHasImportedFirstModel().collect { - _showModelImportTooltip.value = !it - } - } - launch { - appPreferences.userHasChattedWithModel().collect { - _showChatTooltip.value = !it - } - } - launch { - appPreferences.userHasNavigatedToManagement().collect { - _showModelManagementTooltip.value = !it - } - } - } - } - - fun waiveChatTooltip() { - viewModelScope.launch { - appPreferences.setUserHasChattedWithModel(true) - } - } - fun waiveModelImportTooltip() { - viewModelScope.launch { - appPreferences.setUserHasImportedFirstModel(true) - } - } - - fun waiveModelManagementTooltip() { - viewModelScope.launch { - appPreferences.setUserHasNavigatedToManagement(true) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelLoadingViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelLoadingViewModel.kt deleted file mode 100644 index 536f9dbd40..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelLoadingViewModel.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.lifecycle.viewModelScope -import com.arm.aiplayground.data.model.SystemPrompt -import com.arm.aiplayground.data.repo.ModelRepository -import com.arm.aiplayground.data.repo.SystemPromptRepository -import com.arm.aiplayground.engine.ModelLoadingMetrics -import com.arm.aiplayground.engine.ModelLoadingService -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class ModelLoadingViewModel @Inject constructor( - private val modelLoadingService: ModelLoadingService, - private val systemPromptRepository: SystemPromptRepository, - private val modelRepository: ModelRepository, -) : ModelUnloadingViewModel(modelLoadingService) { - - /** - * Currently selected model to be loaded - */ - val selectedModel = modelLoadingService.currentSelectedModel - - /** - * Preset prompts - */ - val presetPrompts: StateFlow> = systemPromptRepository.getPresetPrompts() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS), - initialValue = emptyList() - ) - - /** - * Recent prompts - */ - val recentPrompts: StateFlow> = systemPromptRepository.getRecentPrompts() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS), - initialValue = emptyList() - ) - - /** - * Save a prompt to the recents list. - */ - fun savePromptToRecents(prompt: SystemPrompt) { - viewModelScope.launch { - systemPromptRepository.savePromptToRecents(prompt) - } - } - - /** - * Create and save a custom prompt. - */ - fun saveCustomPromptToRecents(content: String) { - viewModelScope.launch { - systemPromptRepository.saveCustomPrompt(content) - } - } - - /** - * Delete a prompt by ID. - */ - fun deletePrompt(id: String) { - viewModelScope.launch { - systemPromptRepository.deletePrompt(id) - } - } - - /** - * Clear all recent prompts. - */ - fun clearRecentPrompts() { - viewModelScope.launch { - systemPromptRepository.deleteAllPrompts() - } - } - - /** - * Loads the model, then navigate to [BenchmarkScreen] with [ModelLoadingMetrics] - */ - fun onBenchmarkSelected(onNavigateToBenchmark: (ModelLoadingMetrics) -> Unit) = - viewModelScope.launch { - selectedModel.value?.let { model -> - modelLoadingService.loadModelForBenchmark()?.let { metrics -> - modelRepository.updateModelLastUsed(model.id) - onNavigateToBenchmark(metrics) - } - } - } - - /** - * Loads the model, process system prompt if any, - * then navigate to [ConversationScreen] with [ModelLoadingMetrics] - */ - fun onConversationSelected( - systemPrompt: String? = null, - onNavigateToConversation: (ModelLoadingMetrics) -> Unit - ) = viewModelScope.launch { - selectedModel.value?.let { model -> - modelLoadingService.loadModelForConversation(systemPrompt)?.let { metrics -> - modelRepository.updateModelLastUsed(model.id) - onNavigateToConversation(metrics) - } - } - } - - companion object { - private val TAG = ModelLoadingViewModel::class.java.simpleName - - private const val SUBSCRIPTION_TIMEOUT_MS = 5000L - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelUnloadingViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelUnloadingViewModel.kt deleted file mode 100644 index 1ac219ef11..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelUnloadingViewModel.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.arm.aichat.InferenceEngine -import com.arm.aichat.InferenceEngine.State -import com.arm.aichat.isModelLoaded -import com.arm.aichat.isUninterruptible -import com.arm.aiplayground.engine.InferenceService -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - - -/** - * UI states to be consumed by [ModelUnloadDialogHandler], etc. - */ -sealed class UnloadModelState { - object Hidden : UnloadModelState() - object Confirming : UnloadModelState() - object Unloading : UnloadModelState() - data class Error(val message: String) : UnloadModelState() -} - -/** - * Base ViewModel class for screens that requires additional model unloading functionality - */ -abstract class ModelUnloadingViewModel( - private val inferenceService: InferenceService -) : ViewModel() { - - /** - * [InferenceEngine]'s core state - */ - val engineState: StateFlow = inferenceService.engineState - - /** - * Determine if the screen is in an uninterruptible state - * - * Subclass can override this default implementation - */ - protected open val isUninterruptible: Boolean - get() = engineState.value.isUninterruptible - - protected open val isModelLoaded: Boolean - get() = engineState.value.isModelLoaded - - /** - * [UnloadModelConfirmationDialog]'s UI states - */ - private val _unloadModelState = MutableStateFlow(UnloadModelState.Hidden) - val unloadModelState: StateFlow = _unloadModelState.asStateFlow() - - /** - * Handle back press from both back button and top bar - * - * Subclass can override this default implementation - */ - open fun onBackPressed(onNavigateBack: () -> Unit) = - if (isUninterruptible) { - // During uninterruptible operations, ignore back navigation requests - } else if (!isModelLoaded) { - // If model not loaded, no need to unload at all, directly perform back navigation - onNavigateBack.invoke() - } else { - // If model is loaded, show confirmation dialog - _unloadModelState.value = UnloadModelState.Confirming - } - - /** - * Handle confirmation from unload dialog - */ - fun onUnloadConfirmed(onNavigateBack: () -> Unit) = - viewModelScope.launch { - // Set unloading state to show progress - _unloadModelState.value = UnloadModelState.Unloading - - try { - // Perform screen-specific cleanup - performCleanup() - - // Unload the model - inferenceService.unloadModel() - - // Reset state and navigate back - _unloadModelState.value = UnloadModelState.Hidden - onNavigateBack() - } catch (e: Exception) { - // Handle error - _unloadModelState.value = UnloadModelState.Error( - e.message ?: "Unknown error while unloading the model" - ) - } - } - - /** - * Handle dismissal of unload dialog - */ - fun onUnloadDismissed() = - when (_unloadModelState.value) { - is UnloadModelState.Unloading -> { - // Ignore dismissing requests during unloading - } - else -> _unloadModelState.value = UnloadModelState.Hidden - } - - /** - * Perform any screen-specific cleanup before unloading the model - * - * To be implemented by subclasses if needed - */ - protected open suspend fun performCleanup() {} -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelsManagementViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelsManagementViewModel.kt deleted file mode 100644 index 87fa570262..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelsManagementViewModel.kt +++ /dev/null @@ -1,334 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Context.RECEIVER_EXPORTED -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.arm.aichat.gguf.InvalidFileFormatException -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.data.repo.InsufficientStorageException -import com.arm.aiplayground.data.repo.ModelRepository -import com.arm.aiplayground.data.source.remote.HuggingFaceDownloadInfo -import com.arm.aiplayground.data.source.remote.HuggingFaceModel -import com.arm.aiplayground.data.source.remote.HuggingFaceModelDetails -import com.arm.aiplayground.monitoring.MemoryMetrics -import com.arm.aiplayground.util.formatFileByteSize -import com.arm.aiplayground.util.getFileNameFromUri -import com.arm.aiplayground.util.getFileSizeFromUri -import com.arm.aiplayground.viewmodel.ModelManagementState.Deletion -import com.arm.aiplayground.viewmodel.ModelManagementState.Download -import com.arm.aiplayground.viewmodel.ModelManagementState.Importation -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.io.FileNotFoundException -import java.io.IOException -import java.net.SocketTimeoutException -import java.net.UnknownHostException -import javax.inject.Inject -import kotlin.coroutines.cancellation.CancellationException - -@HiltViewModel -class ModelsManagementViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val modelRepository: ModelRepository, -) : ViewModel() { - // UI state: models selected to be batch-deleted - private val _selectedModelsToDelete = MutableStateFlow>(emptyMap()) - val selectedModelsToDelete: StateFlow> = _selectedModelsToDelete.asStateFlow() - - fun toggleModelSelectionById(filteredModels: List, modelId: String) { - val current = _selectedModelsToDelete.value.toMutableMap() - val model = filteredModels.find { it.id == modelId } - - if (model != null) { - if (current.containsKey(modelId)) { - current.remove(modelId) - } else { - current[modelId] = model - } - _selectedModelsToDelete.value = current - } - } - - fun selectModelsToDelete(models: List) { - _selectedModelsToDelete.value = models.associateBy { it.id } - } - - fun clearSelectedModelsToDelete() { - _selectedModelsToDelete.value = emptyMap() - } - - // UI state: import menu - private val _showImportModelMenu = MutableStateFlow(false) - val showImportModelMenu: StateFlow = _showImportModelMenu.asStateFlow() - - fun toggleImportMenu(show: Boolean) { - _showImportModelMenu.value = show - } - - // HuggingFace: ongoing query jobs - private var huggingFaceQueryJob: Job? = null - - // HuggingFace: Ongoing download jobs - private val activeDownloads = mutableMapOf() - private val downloadReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1).let { id -> - if (id in activeDownloads) { - handleDownloadComplete(id) - } - } - } - } - - // Internal state - private val _managementState = MutableStateFlow(ModelManagementState.Idle) - val managementState: StateFlow = _managementState.asStateFlow() - - fun resetManagementState() { - huggingFaceQueryJob?.let { - if (it.isActive) { it.cancel() } - } - clearSelectedModelsToDelete() - _managementState.value = ModelManagementState.Idle - } - - init { - context.registerReceiver( - downloadReceiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - RECEIVER_EXPORTED - ) - } - - /** - * First show confirmation instead of starting import local file immediately - */ - fun importLocalModelFileSelected(uri: Uri) = viewModelScope.launch { - try { - val fileName = getFileNameFromUri(context, uri) ?: throw FileNotFoundException("File size N/A") - val fileSize = getFileSizeFromUri(context, uri) ?: throw FileNotFoundException("File name N/A") - _managementState.value = Importation.Confirming(uri, fileName, fileSize) - } catch (e: Exception) { - _managementState.value = Importation.Error( - message = e.message ?: "Unknown error preparing import" - ) - } - } - - - /** - * Import a local model file from device storage while updating UI states with realtime progress - */ - fun importLocalModelFileConfirmed(uri: Uri, fileName: String, fileSize: Long) = viewModelScope.launch { - try { - _managementState.value = Importation.Importing(0f, fileName, fileSize) - val model = modelRepository.importModel(uri, fileName, fileSize) { progress -> - _managementState.value = Importation.Importing(progress, fileName, fileSize) - } - _managementState.value = Importation.Success(model) - } catch (_: InvalidFileFormatException) { - _managementState.value = Importation.Error( - message = "Not a valid GGUF model!", - learnMoreUrl = "https://huggingface.co/docs/hub/en/gguf", - ) - } catch (e: InsufficientStorageException) { - _managementState.value = Importation.Error( - message = e.message ?: "Insufficient storage space to import $fileName", - learnMoreUrl = "https://support.google.com/android/answer/7431795?hl=en", - ) - } catch (e: Exception) { - Log.e(TAG, "Unknown exception importing $fileName", e) - _managementState.value = Importation.Error( - message = e.message ?: "Unknown error importing $fileName", - ) - } - } - - fun cancelOngoingLocalModelImport() = viewModelScope.launch { - viewModelScope.launch { - // First update UI to show we're attempting to cancel - _managementState.update { current -> - if (current is Importation.Importing) { - current.copy(isCancelling = true) - } else { - current - } - } - - // Attempt to cancel - when (modelRepository.cancelImport()) { - null, true -> { _managementState.value = ModelManagementState.Idle } - false -> { - _managementState.value = Importation.Error( - message = "Failed to cancel import. Try again later." - ) - } - } - } - } - - /** - * Query models on HuggingFace available for download even without signing in - */ - fun queryModelsFromHuggingFace(memoryUsage: MemoryMetrics) { - huggingFaceQueryJob = viewModelScope.launch { - _managementState.emit(Download.Querying) - try { - val models = modelRepository.fetchPreselectedHuggingFaceModels(memoryUsage) - .map(HuggingFaceModelDetails::toModel) - _managementState.emit(Download.Ready(models)) - } catch (_: CancellationException) { - resetManagementState() - } catch (_: UnknownHostException) { - _managementState.value = Download.Error(message = "No internet connection") - } catch (_: SocketTimeoutException) { - _managementState.value = Download.Error(message = "Connection timed out") - } catch (_: FileNotFoundException) { - _managementState.emit(Download.Error(message = "No eligible models")) - } catch (e: IOException) { - _managementState.value = Download.Error(message = "Network error: ${e.message}") - } catch (e: Exception) { - _managementState.emit(Download.Error(message = e.message ?: "Unknown error")) - } - } - } - - /** - * Dispatch download request to [DownloadManager] and update UI - */ - fun downloadHuggingFaceModelConfirmed(model: HuggingFaceModel) = viewModelScope.launch { - try { - require(!model.gated) { "Model is gated!" } - require(!model.private) { "Model is private!" } - val downloadInfo = model.toDownloadInfo() - requireNotNull(downloadInfo) { "Download URL is missing!" } - - modelRepository.getHuggingFaceModelFileSize(downloadInfo).fold( - onSuccess = { actualSize -> - Log.d(TAG, "Model file size: ${formatFileByteSize(actualSize)}") - modelRepository.downloadHuggingFaceModel(downloadInfo, actualSize) - .onSuccess { downloadId -> - activeDownloads[downloadId] = model - _managementState.value = Download.Dispatched(downloadInfo) - } - .onFailure { throw it } - }, - onFailure = { throw it } - ) - } catch (_: UnknownHostException) { - _managementState.value = Download.Error(message = "No internet connection") - } catch (_: SocketTimeoutException) { - _managementState.value = Download.Error(message = "Connection timed out") - } catch (e: IOException) { - _managementState.value = Download.Error(message = "Network error: ${e.message}") - } catch (e: InsufficientStorageException) { - _managementState.value = Download.Error( - message = e.message ?: "Insufficient storage space to download ${model.modelId}", - ) - } catch (e: Exception) { - _managementState.value = Download.Error( - message = e.message ?: "Unknown error downloading ${model.modelId}", - ) - } - } - - private fun handleDownloadComplete(downloadId: Long) = viewModelScope.launch { - val model = activeDownloads.remove(downloadId) ?: return@launch - - (context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager) - .getUriForDownloadedFile(downloadId)?.let { uri -> - try { - val fileName = getFileNameFromUri(context, uri) ?: throw FileNotFoundException("File size N/A") - val fileSize = getFileSizeFromUri(context, uri) ?: throw FileNotFoundException("File name N/A") - _managementState.emit(Download.Completed(model, uri, fileName, fileSize)) - } catch (e: Exception) { - _managementState.value = Download.Error( - message = e.message ?: "Unknown error downloading ${model.modelId}" - ) - } - } - } - - /** - * First show confirmation instead of starting deletion immediately - */ - fun batchDeletionClicked(models: Map) { - _managementState.value = Deletion.Confirming(models) - } - - /** - * Delete multiple models one by one while updating UI states with realtime progress - */ - fun deleteModels(modelsToDelete: Map) = viewModelScope.launch { - val total = modelsToDelete.size - if (total == 0) return@launch - - try { - _managementState.value = Deletion.Deleting(0f, modelsToDelete) - var deleted = 0 - modelsToDelete.keys.toList().forEach { - modelRepository.deleteModel(it) - deleted++ - _managementState.value = Deletion.Deleting(deleted.toFloat() / total, modelsToDelete) - } - _managementState.value = Deletion.Success(modelsToDelete.values.toList()) - clearSelectedModelsToDelete() - - // Reset state after a delay - delay(DELETE_SUCCESS_RESET_TIMEOUT_MS) - _managementState.value = ModelManagementState.Idle - } catch (e: Exception) { - _managementState.value = Deletion.Error( - message = e.message ?: "Error deleting $total models" - ) - } - } - - companion object { - private val TAG = ModelsManagementViewModel::class.java.simpleName - - private const val FETCH_HUGGINGFACE_MODELS_LIMIT_SIZE = 50 - private const val DELETE_SUCCESS_RESET_TIMEOUT_MS = 1000L - } -} - - -sealed class ModelManagementState { - object Idle : ModelManagementState() - - sealed class Importation : ModelManagementState() { - data class Confirming(val uri: Uri, val fileName: String, val fileSize: Long) : Importation() - data class Importing(val progress: Float = 0f, val fileName: String, val fileSize: Long, val isCancelling: Boolean = false) : Importation() - data class Success(val model: ModelInfo) : Importation() - data class Error(val message: String, val learnMoreUrl: String? = null) : Importation() - } - - sealed class Download: ModelManagementState() { - object Querying : Download() - data class Ready(val models: List) : Download() - data class Dispatched(val downloadInfo: HuggingFaceDownloadInfo) : Download() - data class Completed(val model: HuggingFaceModel, val uri: Uri, val fileName: String, val fileSize: Long) : Download() - data class Error(val message: String) : Download() - } - - sealed class Deletion : ModelManagementState() { - data class Confirming(val models: Map): ModelManagementState() - data class Deleting(val progress: Float = 0f, val models: Map) : ModelManagementState() - data class Success(val models: List) : Deletion() - data class Error(val message: String) : Deletion() - } -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelsViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelsViewModel.kt deleted file mode 100644 index 38a1b9c6c0..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/ModelsViewModel.kt +++ /dev/null @@ -1,289 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.arm.aiplayground.data.model.ModelFilter -import com.arm.aiplayground.data.model.ModelInfo -import com.arm.aiplayground.data.model.ModelSortOrder -import com.arm.aiplayground.data.model.filterBy -import com.arm.aiplayground.data.model.queryBy -import com.arm.aiplayground.data.model.sortByOrder -import com.arm.aiplayground.data.repo.ModelRepository -import com.arm.aiplayground.engine.InferenceService -import com.arm.aiplayground.monitoring.PerformanceMonitor -import com.arm.aiplayground.viewmodel.PreselectedModelToRun.RamWarning -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - - -@OptIn(FlowPreview::class) -@HiltViewModel -class ModelsViewModel @Inject constructor( - private val modelRepository: ModelRepository, - private val performanceMonitor: PerformanceMonitor, - private val inferenceService: InferenceService, -) : ViewModel() { - - // UI state: model management mode - private val _allModels = MutableStateFlow?>(null) - val allModels = _allModels.asStateFlow() - - private val _modelScreenUiMode = MutableStateFlow(ModelScreenUiMode.BROWSING) - val modelScreenUiMode = _modelScreenUiMode.asStateFlow() - - fun toggleMode(newMode: ModelScreenUiMode): Boolean { - val oldMode = _modelScreenUiMode.value - when (oldMode) { - ModelScreenUiMode.BROWSING -> { - when (newMode) { - ModelScreenUiMode.SEARCHING -> { - resetPreselection() - } - ModelScreenUiMode.MANAGING -> { - resetPreselection() - } - ModelScreenUiMode.DELETING -> { return false } - else -> { /* No-op */ } - } - } - ModelScreenUiMode.SEARCHING -> { - when (newMode) { - ModelScreenUiMode.BROWSING -> { - searchFieldState.clearText() - } - else -> { return false } - } - } - ModelScreenUiMode.MANAGING -> { - when (newMode) { - ModelScreenUiMode.SEARCHING -> { return false } - else -> { /* No-op */ } - } - } - ModelScreenUiMode.DELETING -> { - when (newMode) { - ModelScreenUiMode.BROWSING, ModelScreenUiMode.SEARCHING -> { return false } - else -> { /* No-op */ } - } - } - } - _modelScreenUiMode.value = newMode - return true - } - - // UI state: search mode - val searchFieldState = TextFieldState() - - // UI state: sort menu - private val _sortOrder = MutableStateFlow(ModelSortOrder.LAST_USED) - val sortOrder = _sortOrder.asStateFlow() - - fun setSortOrder(order: ModelSortOrder) { - _sortOrder.value = order - } - - private val _showSortMenu = MutableStateFlow(false) - val showSortMenu = _showSortMenu.asStateFlow() - - fun toggleSortMenu(visible: Boolean) { - _showSortMenu.value = visible - } - - // UI state: filter menu - private val _activeFilters = MutableStateFlow>( - ModelFilter.ALL_FILTERS.associateWith { false } - ) - val activeFilters: StateFlow> = _activeFilters.asStateFlow() - - fun toggleFilter(filter: ModelFilter, enabled: Boolean) { - _activeFilters.update { current -> - current.toMutableMap().apply { - this[filter] = enabled - } - } - } - - fun clearFilters() { - _activeFilters.update { current -> - current.mapValues { false } - } - } - - private val _showFilterMenu = MutableStateFlow(false) - val showFilterMenu = _showFilterMenu.asStateFlow() - - fun toggleFilterMenu(visible: Boolean) { - _showFilterMenu.value = visible - } - - // Data: filtered & sorted models - private val _filteredModels = MutableStateFlow?>(null) - val filteredModels = _filteredModels.asStateFlow() - - // Data: queried models - private val _queryResults = MutableStateFlow?>(null) - val queryResults = _queryResults.asStateFlow() - - // Data: pre-selected model in expansion mode - private val _preselectedModelToRun = MutableStateFlow(null) - private val _showRamWarning = MutableStateFlow(false) - val preselectedModelToRun = combine( - _preselectedModelToRun, - performanceMonitor.monitorMemoryUsage(), - _showRamWarning, - ) { model, memory, show -> - if (model == null) { - null - } else { - if (memory.availableMem >= model.sizeInBytes + RAM_LOAD_MODEL_BUFFER_BYTES) { - PreselectedModelToRun(model, null) - } else { - PreselectedModelToRun(model, RamWarning(model.sizeInBytes, memory.availableMem, show)) - } - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS), - initialValue = null - ) - - - init { - viewModelScope.launch { - launch { - modelRepository.getModels().collectLatest { - _allModels.value = it - } - } - - launch { - combine( - _allModels, - _activeFilters, - _sortOrder, - ) { models, filters, sortOrder -> - models?.filterBy(filters)?.sortByOrder(sortOrder) - }.collectLatest { - _filteredModels.value = it - } - } - - launch { - combine( - _allModels, - snapshotFlow { searchFieldState.text }.debounce(QUERY_DEBOUNCE_TIMEOUT_MS) - ) { models, query -> - if (query.isBlank()) { - emptyList() - } else { - models?.queryBy(query.toString())?.sortedBy { - it.dateLastUsed ?: it.dateAdded - } - } - }.collectLatest { - _queryResults.value = it - } - } - } - } - - /** - * Pre-select a model to expand its details and show Run FAB - */ - fun preselectModel(modelInfo: ModelInfo, preselected: Boolean) { - _preselectedModelToRun.value = if (preselected) modelInfo else null - _showRamWarning.value = false - } - - /** - * Reset preselected model to none (before navigating away) - */ - fun resetPreselection() { - _preselectedModelToRun.value = null - _showRamWarning.value = false - } - - /** - * Select the currently pre-selected model - * - * @return True if RAM enough, otherwise False. - */ - fun selectModel(preselectedModelToRun: PreselectedModelToRun) = - when (preselectedModelToRun.ramWarning?.showing) { - null -> { - inferenceService.setCurrentModel(preselectedModelToRun.modelInfo) - true - } - false -> { - _showRamWarning.value = true - false - } - else -> false - } - - /** - * Dismiss the RAM warnings - */ - fun dismissRamWarning() { - _showRamWarning.value = false - } - - /** - * Acknowledge RAM warnings and confirm currently pre-selected model - * - * @return True if confirmed, otherwise False. - */ - fun confirmSelectedModel(modelInfo: ModelInfo, ramWarning: RamWarning): Boolean = - if (ramWarning.showing) { - inferenceService.setCurrentModel(modelInfo) - _showRamWarning.value = false - - resetPreselection() - - true - } else { - false - } - - - companion object { - private val TAG = ModelsViewModel::class.java.simpleName - - private const val SUBSCRIPTION_TIMEOUT_MS = 5000L - private const val QUERY_DEBOUNCE_TIMEOUT_MS = 500L - - private const val RAM_LOAD_MODEL_BUFFER_BYTES = 300 * 1024 - } -} - -enum class ModelScreenUiMode { - BROWSING, - SEARCHING, - MANAGING, - DELETING -} - -data class PreselectedModelToRun( - val modelInfo: ModelInfo, - val ramWarning: RamWarning?, -) { - data class RamWarning( - val requiredRam: Long, - val availableRam: Long, - val showing: Boolean, - ) -} diff --git a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/SettingsViewModel.kt b/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/SettingsViewModel.kt deleted file mode 100644 index c5d2c45c6e..0000000000 --- a/examples/llama.android/app/src/main/java/com/arm/aiplayground/viewmodel/SettingsViewModel.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.arm.aiplayground.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.arm.aichat.ArmCpuTier -import com.arm.aichat.TierDetection -import com.arm.aiplayground.data.repo.ModelRepository -import com.arm.aiplayground.data.source.prefs.ColorThemeMode -import com.arm.aiplayground.data.source.prefs.DarkThemeMode -import com.arm.aiplayground.data.source.prefs.UserPreferences -import com.arm.aiplayground.monitoring.BatteryMetrics -import com.arm.aiplayground.monitoring.MemoryMetrics -import com.arm.aiplayground.monitoring.PerformanceMonitor -import com.arm.aiplayground.monitoring.StorageMetrics -import com.arm.aiplayground.monitoring.TemperatureMetrics -import com.arm.aiplayground.monitoring.TemperatureWarningLevel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * ViewModel that manages performance monitoring for the app. - */ -@HiltViewModel -class SettingsViewModel @Inject constructor( - private val userPreferences: UserPreferences, - private val performanceMonitor: PerformanceMonitor, - private val modelRepository: ModelRepository, - private val tierDetection: TierDetection, -) : ViewModel() { - - // Storage usage metrics - private val _storageMetrics = MutableStateFlow(null) - val storageMetrics: StateFlow = _storageMetrics.asStateFlow() - - // Memory usage metrics - private val _memoryUsage = MutableStateFlow(MemoryMetrics(0, 0, 0, 0f, 0f)) - val memoryUsage: StateFlow = _memoryUsage.asStateFlow() - - // Battery information - private val _batteryInfo = MutableStateFlow(BatteryMetrics(0, false)) - val batteryInfo: StateFlow = _batteryInfo.asStateFlow() - - // User preferences: monitoring - private val _temperatureMetrics = MutableStateFlow(TemperatureMetrics(0f, TemperatureWarningLevel.NORMAL)) - val temperatureMetrics: StateFlow = _temperatureMetrics.asStateFlow() - - private val _isMonitoringEnabled = MutableStateFlow(true) - val isMonitoringEnabled: StateFlow = _isMonitoringEnabled.asStateFlow() - - private val _useFahrenheitUnit = MutableStateFlow(false) - val useFahrenheitUnit: StateFlow = _useFahrenheitUnit.asStateFlow() - - private val _monitoringInterval = MutableStateFlow(5000L) - val monitoringInterval: StateFlow = _monitoringInterval.asStateFlow() - - // User preferences: themes - private val _colorThemeMode = MutableStateFlow(ColorThemeMode.ARM) - val colorThemeMode: StateFlow = _colorThemeMode.asStateFlow() - - private val _darkThemeMode = MutableStateFlow(DarkThemeMode.AUTO) - val darkThemeMode: StateFlow = _darkThemeMode.asStateFlow() - - val detectedTier: ArmCpuTier? - get() = tierDetection.getDetectedTier() - - init { - viewModelScope.launch { - // Load user preferences - _isMonitoringEnabled.value = userPreferences.isPerformanceMonitoringEnabled().first() - _useFahrenheitUnit.value = userPreferences.usesFahrenheitTemperature().first() - _monitoringInterval.value = userPreferences.getMonitoringInterval().first() - _colorThemeMode.value = userPreferences.getColorThemeMode().first() - _darkThemeMode.value = userPreferences.getDarkThemeMode().first() - - // Start monitoring if enabled - if (_isMonitoringEnabled.value) { - startMonitoring() - } - } - } - - /** - * Starts monitoring device performance. - */ - private var monitoringJob: Job? = null - private fun startMonitoring() { - val interval = _monitoringInterval.value - - monitoringJob?.cancel() - viewModelScope.launch { - launch { - modelRepository.getStorageMetrics().collect { metrics -> - _storageMetrics.value = metrics - } - } - - launch { - performanceMonitor.monitorMemoryUsage(interval).collect { metrics -> - _memoryUsage.value = metrics - } - } - - launch { - performanceMonitor.monitorBattery(interval * 2).collect { metrics -> - _batteryInfo.value = metrics - } - } - - launch { - performanceMonitor.monitorTemperature(interval * 2).collect { metrics -> - _temperatureMetrics.value = metrics - } - } - } - } - - /** - * Sets whether performance monitoring is enabled. - */ - fun setMonitoringEnabled(enabled: Boolean) { - viewModelScope.launch { - if (enabled && !_isMonitoringEnabled.value) { - startMonitoring() - } - _isMonitoringEnabled.value = enabled - userPreferences.setPerformanceMonitoringEnabled(enabled) - } - } - - /** - * Sets the temperature unit preference. - */ - fun setUseFahrenheitUnit(useFahrenheit: Boolean) { - viewModelScope.launch { - userPreferences.setUseFahrenheitTemperature(useFahrenheit) - _useFahrenheitUnit.value = useFahrenheit - } - } - - /** - * Sets the monitoring interval. - */ - fun setMonitoringInterval(intervalMs: Long) { - viewModelScope.launch { - // Restart monitoring with new interval if active - if (_isMonitoringEnabled.value) { - startMonitoring() - } - userPreferences.setMonitoringInterval(intervalMs) - _monitoringInterval.value = intervalMs - } - } - - /** - * Sets the color theme mode. - */ - fun setColorThemeMode(mode: ColorThemeMode) { - viewModelScope.launch { - userPreferences.setColorThemeMode(mode) - _colorThemeMode.value = mode - } - } - - /** - * Sets the dark theme mode. - */ - fun setDarkThemeMode(mode: DarkThemeMode) { - viewModelScope.launch { - userPreferences.setDarkThemeMode(mode) - _darkThemeMode.value = mode - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt new file mode 100644 index 0000000000..4923e8e764 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt @@ -0,0 +1,266 @@ +package com.example.llama + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.arm.aichat.AiChat +import com.arm.aichat.InferenceEngine +import com.arm.aichat.TierDetection +import com.arm.aichat.gguf.GgufMetadata +import com.arm.aichat.gguf.GgufMetadataReader +import com.google.android.material.floatingactionbutton.FloatingActionButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.util.UUID + +class MainActivity : AppCompatActivity() { + + // Android views + private lateinit var tierTv: TextView + private lateinit var pickerBtn: FloatingActionButton + private lateinit var ggufTv: TextView + private lateinit var messagesRv: RecyclerView + private lateinit var userInputEt: EditText + private lateinit var userSendBtn: FloatingActionButton + + // Arm AI Chat engine and utils + private lateinit var detection: TierDetection + private lateinit var engine: InferenceEngine + + // Conversation states + private val messages = mutableListOf() + private val lastAssistantMsg = StringBuilder() + private val messageAdapter = MessageAdapter(messages) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_main) + + // Find views + tierTv = findViewById(R.id.tier) + pickerBtn = findViewById(R.id.pick_model) + ggufTv = findViewById(R.id.gguf) + messagesRv = findViewById(R.id.messages) + messagesRv.layoutManager = LinearLayoutManager(this) + messagesRv.adapter = messageAdapter + userInputEt = findViewById(R.id.user_input) + userSendBtn = findViewById(R.id.user_send) + + // Arm AI Chat initialization + lifecycleScope.launch(Dispatchers.Default) { + // Obtain the device's CPU feature tier + detection = AiChat.getTierDetection(applicationContext) + withContext(Dispatchers.Main) { + tierTv.text = detection.getDetectedTier()?.description ?: "N/A" + } + + // Obtain the inference engine + engine = AiChat.getInferenceEngine(applicationContext) + } + + // Upon file picker button tapped, prompt user to select a GGUF metadata on the device + pickerBtn.setOnClickListener { + getContent.launch(arrayOf("*/*")) + } + + // Upon user send button tapped, validate input and send to engine + userSendBtn.setOnClickListener { + handleUserInput() + } + } + + private val getContent = registerForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> + Log.i(TAG, "Selected file uri:\n $uri") + uri?.let { handleSelectedModel(it) } + } + + /** + * Handles the file Uri from [getContent] result + */ + private fun handleSelectedModel(uri: Uri) { + // Update UI states + pickerBtn.isEnabled = false + userInputEt.hint = "Parsing GGUF..." + ggufTv.text = "Parsing metadata from selected file \n$uri" + + lifecycleScope.launch(Dispatchers.IO) { + // Parse GGUF metadata + Log.i(TAG, "Parsing GGUF metadata...") + contentResolver.openInputStream(uri)?.use { + GgufMetadataReader.create().readStructuredMetadata(it) + }?.let { metadata -> + // Update UI to show GGUF metadata to user + Log.i(TAG, "GGUF parsed: \n$metadata") + withContext(Dispatchers.Main) { + ggufTv.text = metadata.toString() + } + + // Ensure the model file is available + val modelName = metadata.filename() + FILE_EXTENSION_GGUF + contentResolver.openInputStream(uri)?.use { input -> + ensureModelFile(modelName, input) + }?.let { modelFile -> + loadModel(modelName, modelFile) + + withContext(Dispatchers.Main) { + userInputEt.hint = "Type and send a message!" + userInputEt.isEnabled = true + userSendBtn.isEnabled = true + } + } + } + } + } + + /** + * Prepare the model file within app's private storage + */ + private suspend fun ensureModelFile(modelName: String, input: InputStream) = + withContext(Dispatchers.IO) { + File(ensureModelsDirectory(), modelName).also { file -> + // Copy the file into local storage if not yet done + if (!file.exists()) { + Log.i(TAG, "Start copying file to $modelName") + withContext(Dispatchers.Main) { + userInputEt.hint = "Copying file..." + } + + FileOutputStream(file).use { input.copyTo(it) } + Log.i(TAG, "Finished copying file to $modelName") + } else { + Log.i(TAG, "File already exists $modelName") + } + } + } + + /** + * Load the model file from the app private storage + */ + private suspend fun loadModel(modelName: String, modelFile: File) = + withContext(Dispatchers.IO) { + Log.i(TAG, "Loading model $modelName") + withContext(Dispatchers.Main) { + userInputEt.hint = "Loading model..." + } + engine.loadModel(modelFile.path) + } + + /** + * Validate and send the user message into [InferenceEngine] + */ + private fun handleUserInput() { + userInputEt.text.toString().also { userSsg -> + if (userSsg.isEmpty()) { + Toast.makeText(this, "Input message is empty!", Toast.LENGTH_SHORT).show() + } else { + userInputEt.text = null + userSendBtn.isEnabled = false + + // Update message states + messages.add(Message(UUID.randomUUID().toString(), userSsg, true)) + lastAssistantMsg.clear() + messages.add(Message(UUID.randomUUID().toString(), lastAssistantMsg.toString(), false)) + + lifecycleScope.launch(Dispatchers.Default) { + engine.sendUserPrompt(userSsg) + .onCompletion { + withContext(Dispatchers.Main) { + userSendBtn.isEnabled = true + } + }.collect { token -> + val messageCount = messages.size + check(messageCount > 0 && !messages[messageCount - 1].isUser) + + messages.removeAt(messageCount - 1).copy( + content = lastAssistantMsg.append(token).toString() + ).let { messages.add(it) } + + withContext(Dispatchers.Main) { + messageAdapter.notifyItemChanged(messages.size - 1) + } + } + } + } + } + } + + /** + * Run a benchmark with the model file + */ + private suspend fun runBenchmark(modelName: String, modelFile: File) = + withContext(Dispatchers.Default) { + Log.i(TAG, "Starts benchmarking $modelName") + withContext(Dispatchers.Main) { + userInputEt.hint = "Running benchmark..." + } + engine.bench( + pp=BENCH_PROMPT_PROCESSING_TOKENS, + tg=BENCH_TOKEN_GENERATION_TOKENS, + pl=BENCH_SEQUENCE, + nr=BENCH_REPETITION + ).let { result -> + messages.add(Message(UUID.randomUUID().toString(), result, false)) + withContext(Dispatchers.Main) { + messageAdapter.notifyItemChanged(messages.size - 1) + } + } + } + + /** + * Create the `models` directory if not exist. + */ + private fun ensureModelsDirectory() = + File(filesDir, DIRECTORY_MODELS).also { + if (it.exists() && !it.isDirectory) { it.delete() } + if (!it.exists()) { it.mkdir() } + } + + companion object { + private val TAG = MainActivity::class.java.simpleName + + private const val DIRECTORY_MODELS = "models" + private const val FILE_EXTENSION_GGUF = ".gguf" + + private const val BENCH_PROMPT_PROCESSING_TOKENS = 512 + private const val BENCH_TOKEN_GENERATION_TOKENS = 128 + private const val BENCH_SEQUENCE = 1 + private const val BENCH_REPETITION = 3 + } +} + +fun GgufMetadata.filename() = when { + basic.name != null -> { + basic.name?.let { name -> + basic.sizeLabel?.let { size -> + "$name-$size" + } ?: name + } + } + architecture?.architecture != null -> { + architecture?.architecture?.let { arch -> + basic.uuid?.let { uuid -> + "$arch-$uuid" + } ?: "$arch-${System.currentTimeMillis()}" + } + } + else -> { + "model-${System.currentTimeMillis().toHexString()}" + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MessageAdapter.kt b/examples/llama.android/app/src/main/java/com/example/llama/MessageAdapter.kt new file mode 100644 index 0000000000..0439f96441 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/MessageAdapter.kt @@ -0,0 +1,51 @@ +package com.example.llama + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView + +data class Message( + val id: String, + val content: String, + val isUser: Boolean +) + +class MessageAdapter( + private val messages: List +) : RecyclerView.Adapter() { + + companion object { + private const val VIEW_TYPE_USER = 1 + private const val VIEW_TYPE_ASSISTANT = 2 + } + + override fun getItemViewType(position: Int): Int { + return if (messages[position].isUser) VIEW_TYPE_USER else VIEW_TYPE_ASSISTANT + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + return if (viewType == VIEW_TYPE_USER) { + val view = layoutInflater.inflate(R.layout.item_message_user, parent, false) + UserMessageViewHolder(view) + } else { + val view = layoutInflater.inflate(R.layout.item_message_assistant, parent, false) + AssistantMessageViewHolder(view) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val message = messages[position] + if (holder is UserMessageViewHolder || holder is AssistantMessageViewHolder) { + val textView = holder.itemView.findViewById(R.id.msg_content) + textView.text = message.content + } + } + + override fun getItemCount(): Int = messages.size + + class UserMessageViewHolder(view: View) : RecyclerView.ViewHolder(view) + class AssistantMessageViewHolder(view: View) : RecyclerView.ViewHolder(view) +} diff --git a/examples/llama.android/app/src/main/res/drawable/bg_assistant_message.xml b/examples/llama.android/app/src/main/res/drawable/bg_assistant_message.xml new file mode 100644 index 0000000000..f90c3db458 --- /dev/null +++ b/examples/llama.android/app/src/main/res/drawable/bg_assistant_message.xml @@ -0,0 +1,4 @@ + + + + diff --git a/examples/llama.android/app/src/main/res/drawable/bg_user_message.xml b/examples/llama.android/app/src/main/res/drawable/bg_user_message.xml new file mode 100644 index 0000000000..3ca7daefec --- /dev/null +++ b/examples/llama.android/app/src/main/res/drawable/bg_user_message.xml @@ -0,0 +1,4 @@ + + + + diff --git a/examples/llama.android/app/src/main/res/drawable/ic_launcher_background.xml b/examples/llama.android/app/src/main/res/drawable/ic_launcher_background.xml index 5782842b81..07d5da9cbf 100644 --- a/examples/llama.android/app/src/main/res/drawable/ic_launcher_background.xml +++ b/examples/llama.android/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,4 +1,170 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/llama.android/app/src/main/res/drawable/ic_launcher_foreground.xml b/examples/llama.android/app/src/main/res/drawable/ic_launcher_foreground.xml index 734de92b84..2b068d1146 100644 --- a/examples/llama.android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/examples/llama.android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,17 +1,30 @@ - - - - - - - - + android:viewportWidth="108" + android:viewportHeight="108"> + + + + + + + + + + \ No newline at end of file diff --git a/examples/llama.android/app/src/main/res/drawable/logo_huggingface.xml b/examples/llama.android/app/src/main/res/drawable/logo_huggingface.xml deleted file mode 100644 index a666ce3a2d..0000000000 --- a/examples/llama.android/app/src/main/res/drawable/logo_huggingface.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/examples/llama.android/app/src/main/res/drawable/outline_folder_open_24.xml b/examples/llama.android/app/src/main/res/drawable/outline_folder_open_24.xml new file mode 100644 index 0000000000..f58b501e3b --- /dev/null +++ b/examples/llama.android/app/src/main/res/drawable/outline_folder_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/examples/llama.android/app/src/main/res/drawable/outline_send_24.xml b/examples/llama.android/app/src/main/res/drawable/outline_send_24.xml new file mode 100644 index 0000000000..712adc00c4 --- /dev/null +++ b/examples/llama.android/app/src/main/res/drawable/outline_send_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/examples/llama.android/app/src/main/res/layout/activity_main.xml b/examples/llama.android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..90eda033e7 --- /dev/null +++ b/examples/llama.android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/llama.android/app/src/main/res/layout/item_message_assistant.xml b/examples/llama.android/app/src/main/res/layout/item_message_assistant.xml new file mode 100644 index 0000000000..b7fb500393 --- /dev/null +++ b/examples/llama.android/app/src/main/res/layout/item_message_assistant.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/examples/llama.android/app/src/main/res/layout/item_message_user.xml b/examples/llama.android/app/src/main/res/layout/item_message_user.xml new file mode 100644 index 0000000000..fe871f12fa --- /dev/null +++ b/examples/llama.android/app/src/main/res/layout/item_message_user.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml index b3e26b4c60..6f3b755bf5 100644 --- a/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ b/examples/llama.android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -3,4 +3,4 @@ - + \ No newline at end of file diff --git a/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index e3ecc830fa..0000000000 Binary files a/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000..c209e78ecd Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..b2dfe3d1ba Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 2f585d6bdb..0000000000 Binary files a/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000..4f0f1d64e5 Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..62b611da08 Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index f3f0c56438..0000000000 Binary files a/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000..948a3070fe Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..1b9a6956b3 Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 37fa3b5a28..0000000000 Binary files a/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..28d4b77f9f Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..9287f50836 Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index e45598fbc1..0000000000 Binary files a/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..aa7d6427e6 Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..9126ae37cb Binary files /dev/null and b/examples/llama.android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/examples/llama.android/app/src/main/res/values/strings.xml b/examples/llama.android/app/src/main/res/values/strings.xml index 38d01d5b09..36059fc799 100644 --- a/examples/llama.android/app/src/main/res/values/strings.xml +++ b/examples/llama.android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - AI Playground + AI Chat basic sample diff --git a/examples/llama.android/app/src/main/res/values/themes.xml b/examples/llama.android/app/src/main/res/values/themes.xml index 031444ffd7..2e4fdad72e 100644 --- a/examples/llama.android/app/src/main/res/values/themes.xml +++ b/examples/llama.android/app/src/main/res/values/themes.xml @@ -1,5 +1,10 @@ - + +