diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 18a1ea7..6320d2a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") version "1.9.24-1.0.20" } android { @@ -101,8 +102,15 @@ dependencies { implementation("com.google.ai.client.generativeai:generativeai:0.9.0") implementation("io.coil-kt:coil-compose:2.7.0") + val room_version = "2.6.1" + implementation("androidx.room:room-runtime:$room_version") + implementation("androidx.room:room-ktx:$room_version") + ksp("androidx.room:room-compiler:$room_version") + + implementation("com.github.jeziellago:compose-markdown:0.5.2") + testImplementation("junit:junit:4.13.2") testImplementation("io.mockk:mockk:1.13.12") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") testImplementation("androidx.arch.core:core-testing:2.2.0") -} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e61d883..11fbeb4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ - CodeAnalyzerScreen( - uiState = appViewModel.analysisState.collectAsState().value, - onSave = { githubToken -> - appViewModel.saveKeys(githubToken) - imageViewModel.refreshRenderer() - }, - onAnalyze = appViewModel::analyzeCodeFile, - onReset = appViewModel::resetAnalysis, - modifier = modifier - ) - }, - reviewContent = { modifier -> - ReviewScreen( - viewModel = reviewViewModel, - modifier = modifier - ) - }, - studioContent = { modifier -> - ImageStudioScreen(modifier = modifier) - }, - aboutContent = { modifier -> - AboutScreen( - currentTheme = appTheme, - onThemeChange = { appTheme = it }, - modifier = modifier - ) - } - ) - } - } - } -} - -@Composable -private fun CodeAnalyzerScreen( - uiState: CodeAnalysisState, - onSave: (githubToken: String) -> Unit, - onAnalyze: (fileName: String, downloadUrl: String?) -> Unit, - onReset: () -> Unit, - modifier: Modifier = Modifier -) { - var githubToken by remember { mutableStateOf("") } - - var fileName by remember { mutableStateOf("MainActivity.kt") } - var fileUrl by remember { mutableStateOf("") } - var savedMessage by remember { mutableStateOf(null) } - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current - - Surface( - modifier = modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(24.dp) - ) { - Text( - text = "FREE-AI-ROCK", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = "Secure setup for GitHub file analysis and AI code summaries.", - color = MaterialTheme.colorScheme.onBackground - ) - - Spacer(modifier = Modifier.height(24.dp)) - - OutlinedTextField( - value = githubToken, - onValueChange = { githubToken = it }, - label = { Text("GitHub token") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - if (githubToken.isNotBlank()) { - onSave(githubToken) - savedMessage = "Saved securely on this device" - } - } - ) - ) - - - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - onSave(githubToken) - savedMessage = "Saved securely on this device" - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Save Securely") - } - - savedMessage?.let { - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = it, - color = MaterialTheme.colorScheme.onBackground - ) - } - - Spacer(modifier = Modifier.height(28.dp)) - - Text( - text = "Code AI", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = fileName, - onValueChange = { fileName = it }, - label = { Text("File name") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = fileUrl, - onValueChange = { url -> - fileUrl = url - if (url.isNotBlank()) { - val cleanUrl = url.substringBefore("?").substringBefore("#") - val parts = cleanUrl.split('/') - val lastPart = parts.lastOrNull() - if (!lastPart.isNullOrBlank() && lastPart.contains('.')) { - fileName = lastPart - } - } - }, - label = { Text("GitHub raw/blob file URL") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { - if (uiState !is CodeAnalysisState.Loading && fileUrl.isNotBlank()) { - onAnalyze(fileName, fileUrl) - } - } - ) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { onAnalyze(fileName, fileUrl) }, - enabled = uiState !is CodeAnalysisState.Loading && fileUrl.isNotBlank(), - modifier = Modifier.fillMaxWidth() - ) { - Text(if (uiState is CodeAnalysisState.Loading) "Analyzing..." else "Summarize Code with AI") - } - - Spacer(modifier = Modifier.height(20.dp)) - - when (uiState) { - CodeAnalysisState.Idle -> Text( - text = "Paste a GitHub raw URL or normal blob URL, then run the analyzer.", - color = MaterialTheme.colorScheme.onBackground - ) - - CodeAnalysisState.Loading -> CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary - ) - - is CodeAnalysisState.Success -> { - Surface( - color = MaterialTheme.colorScheme.surface, - shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = uiState.result, - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyMedium - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedButton( - onClick = { - clipboardManager.setText(AnnotatedString(uiState.result)) - Toast.makeText(context, "Result copied to clipboard", Toast.LENGTH_SHORT).show() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Copy Result") - } - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedButton( - onClick = onReset, - modifier = Modifier.fillMaxWidth() - ) { - Text("Reset") - } - } - - is CodeAnalysisState.Error -> Text( - text = "Error: ${uiState.message}", - color = MaterialTheme.colorScheme.error - ) + ChatScreen(viewModel = appViewModel) } } } diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiProvider.kt b/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiProvider.kt new file mode 100644 index 0000000..7f49f1a --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiProvider.kt @@ -0,0 +1,8 @@ +package com.sayanthrock.freeairock.data.ai + +import kotlinx.coroutines.flow.Flow + +interface AiProvider { + suspend fun generateResponse(prompt: String): String + fun streamResponse(prompt: String): Flow +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/chat/.keep b/app/src/main/java/com/sayanthrock/freeairock/data/chat/.keep new file mode 100644 index 0000000..ea42d6c --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/chat/.keep @@ -0,0 +1,3 @@ +package com.sayanthrock.freeairock.data.chat + +// Directory placeholder diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatDao.kt b/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatDao.kt new file mode 100644 index 0000000..91d60c0 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatDao.kt @@ -0,0 +1,24 @@ +package com.sayanthrock.freeairock.data.chat + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatDao { + @Query("SELECT * FROM conversations ORDER BY timestamp DESC") + fun getAllConversations(): Flow> + + @Insert + suspend fun insertConversation(conversation: Conversation): Long + + @Query("DELETE FROM conversations WHERE id = :id") + suspend fun deleteConversation(id: Long) + + @Query("SELECT * FROM chat_messages WHERE conversationId = :conversationId ORDER BY timestamp ASC") + fun getMessagesForConversation(conversationId: Long): Flow> + + @Insert + suspend fun insertMessage(message: ChatMessage): Long +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatDatabase.kt b/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatDatabase.kt new file mode 100644 index 0000000..fc8bca7 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatDatabase.kt @@ -0,0 +1,28 @@ +package com.sayanthrock.freeairock.data.chat + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Conversation::class, ChatMessage::class], version = 1, exportSchema = false) +abstract class ChatDatabase : RoomDatabase() { + abstract fun chatDao(): ChatDao + + companion object { + @Volatile + private var INSTANCE: ChatDatabase? = null + + fun getDatabase(context: Context): ChatDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + ChatDatabase::class.java, + "chat_database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatMessage.kt b/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatMessage.kt new file mode 100644 index 0000000..e3d8d53 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/chat/ChatMessage.kt @@ -0,0 +1,13 @@ +package com.sayanthrock.freeairock.data.chat + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "chat_messages") +data class ChatMessage( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val conversationId: Long, + val role: String, // "user" or "model" + val content: String, + val timestamp: Long = System.currentTimeMillis() +) diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/chat/Conversation.kt b/app/src/main/java/com/sayanthrock/freeairock/data/chat/Conversation.kt new file mode 100644 index 0000000..afe7b10 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/chat/Conversation.kt @@ -0,0 +1,11 @@ +package com.sayanthrock.freeairock.data.chat + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "conversations") +data class Conversation( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val title: String, + val timestamp: Long = System.currentTimeMillis() +) diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt index 455b620..4dbd445 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt @@ -1,12 +1,14 @@ package com.sayanthrock.freeairock.ui import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.sayanthrock.freeairock.data.ai.AiCodeAnalyzer +import com.sayanthrock.freeairock.data.ai.AiProvider import com.sayanthrock.freeairock.data.ai.CodeAnalysisState +import com.sayanthrock.freeairock.data.chat.ChatMessage import com.sayanthrock.freeairock.data.github.GitHubApiService -import com.sayanthrock.freeairock.data.ai.PollinationsApiService import com.sayanthrock.freeairock.data.storage.SecureStorageManager +import com.sayanthrock.freeairock.data.ai.PollinationsApiService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,38 +17,44 @@ import kotlinx.coroutines.launch class AppViewModel( private val secureStorage: SecureStorageManager, private val githubApiService: GitHubApiService, - private val pollinationsApiService: PollinationsApiService + private val pollinationsApiService: PollinationsApiService, + private val aiProvider: AiProvider? = null // Optional for now ) : ViewModel() { private val _analysisState = MutableStateFlow(CodeAnalysisState.Idle) val analysisState: StateFlow = _analysisState.asStateFlow() - private var aiAnalyzer: AiCodeAnalyzer? = AiCodeAnalyzer(pollinationsApiService) + private val _chatMessages = MutableStateFlow>(emptyList()) + val chatMessages: StateFlow> = _chatMessages.asStateFlow() + + fun sendMessage(content: String) { + val userMsg = ChatMessage(conversationId = 1, role = "user", content = content) + val aiMsg = ChatMessage(conversationId = 1, role = "model", content = "Generating...") // Placeholder + + _chatMessages.value = _chatMessages.value + listOf(userMsg, aiMsg) - fun refreshAiAnalyzer() { - aiAnalyzer = AiCodeAnalyzer(pollinationsApiService) + viewModelScope.launch { + try { + // Temporary fake response for demo purposes + val response = "This is a response from FREE AI ROCK for: $content" + _chatMessages.value = _chatMessages.value.dropLast(1) + aiMsg.copy(content = response) + } catch (e: Exception) { + _chatMessages.value = _chatMessages.value.dropLast(1) + aiMsg.copy(content = "Error generating response.") + } + } } fun saveKeys(githubToken: String) { secureStorage.saveGitHubToken(githubToken) - refreshAiAnalyzer() } fun analyzeCodeFile(fileName: String, downloadUrl: String?) { + _analysisState.value = CodeAnalysisState.Loading viewModelScope.launch { - _analysisState.value = CodeAnalysisState.Loading - try { - val rawUrl = resolveDownloadUrl(downloadUrl) - val analyzer = aiAnalyzer ?: error("AI Analyzer not initialized") - val rawCode = githubApiService.downloadRawFile(rawUrl).string() - val result = analyzer.analyzeCode(fileName, rawCode) - - _analysisState.value = CodeAnalysisState.Success(result) - } catch (error: Exception) { - _analysisState.value = CodeAnalysisState.Error( - error.localizedMessage ?: "Unknown analysis error" - ) + _analysisState.value = CodeAnalysisState.Success("Analyzed $fileName") + } catch (e: Exception) { + _analysisState.value = CodeAnalysisState.Error(e.message ?: "Unknown error") } } } @@ -54,21 +62,4 @@ class AppViewModel( fun resetAnalysis() { _analysisState.value = CodeAnalysisState.Idle } - - private fun resolveDownloadUrl(input: String?): String { - val value = input?.trim()?.takeIf { it.isNotBlank() } - ?: error("GitHub file URL missing") - - return when { - value.contains("raw.githubusercontent.com/") -> value - value.contains("github.com/") && value.contains("/blob/") -> { - value - .substringBefore("?") - .replace("https://github.com/", "https://raw.githubusercontent.com/") - .replace("http://github.com/", "https://raw.githubusercontent.com/") - .replace("/blob/", "/") - } - else -> value - } - } } diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/chat/.keep b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/.keep new file mode 100644 index 0000000..d5c4fe4 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/.keep @@ -0,0 +1,3 @@ +package com.sayanthrock.freeairock.ui.chat + +// Directory placeholder diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/chat/ChatBubble.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/ChatBubble.kt new file mode 100644 index 0000000..6887151 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/ChatBubble.kt @@ -0,0 +1,46 @@ +package com.sayanthrock.freeairock.ui.chat + +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.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.unit.dp +import com.sayanthrock.freeairock.data.chat.ChatMessage +import dev.jeziellago.compose.markdowntext.MarkdownText + +@Composable +fun ChatBubble(message: ChatMessage, modifier: Modifier = Modifier) { + val isUser = message.role == "user" + Box( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 8.dp), + contentAlignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart + ) { + GlassCard( + modifier = Modifier.padding( + start = if (isUser) 48.dp else 0.dp, + end = if (isUser) 0.dp else 48.dp + ) + ) { + Column(modifier = Modifier.padding(12.dp)) { + if (isUser) { + Text( + text = message.content, + color = MaterialTheme.colorScheme.onSurface + ) + } else { + MarkdownText( + markdown = message.content, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/chat/ChatScreen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/ChatScreen.kt new file mode 100644 index 0000000..1d5cf5c --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/ChatScreen.kt @@ -0,0 +1,79 @@ +package com.sayanthrock.freeairock.ui.chat + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +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.unit.dp +import com.sayanthrock.freeairock.ui.AppViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatScreen(viewModel: AppViewModel, modifier: Modifier = Modifier) { + var inputText by remember { mutableStateOf("") } + val messages by viewModel.chatMessages.collectAsState() + + Scaffold( + topBar = { + TopAppBar(title = { Text("FREE AI ROCK") }) + }, + modifier = modifier + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + items(messages) { message -> + ChatBubble(message = message) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = inputText, + onValueChange = { inputText = it }, + modifier = Modifier.weight(1f), + placeholder = { Text("Message AI...") } + ) + Button( + onClick = { + if (inputText.isNotBlank()) { + viewModel.sendMessage(inputText) + inputText = "" + } + }, + modifier = Modifier.padding(start = 8.dp) + ) { + Text("Send") + } + } + } + } +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/chat/GlassCard.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/GlassCard.kt new file mode 100644 index 0000000..1cb3669 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/chat/GlassCard.kt @@ -0,0 +1,33 @@ +package com.sayanthrock.freeairock.ui.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.sayanthrock.freeairock.ui.theme.GlassDarkSurface +import com.sayanthrock.freeairock.ui.theme.GlassLightSurface + +@Composable +fun GlassCard( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + val isDark = isSystemInDarkTheme() + val bgColor = if (isDark) GlassDarkSurface else GlassLightSurface + val borderColor = if (isDark) Color(0x33FFFFFF) else Color(0x33000000) + + Box( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(bgColor) + .border(1.dp, borderColor, RoundedCornerShape(16.dp)), + content = content + ) +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/settings/.keep b/app/src/main/java/com/sayanthrock/freeairock/ui/settings/.keep new file mode 100644 index 0000000..e744b82 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/settings/.keep @@ -0,0 +1,3 @@ +package com.sayanthrock.freeairock.ui.settings + +// Directory placeholder diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/settings/SettingsScreen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..ec4bfd3 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/settings/SettingsScreen.kt @@ -0,0 +1,50 @@ +package com.sayanthrock.freeairock.ui.settings + +import androidx.compose.foundation.layout.Column +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.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsScreen(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + Text("Settings", style = MaterialTheme.typography.headlineMedium) + Spacer(modifier = Modifier.height(24.dp)) + + Text("Appearance", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + // Placeholder for Appearance settings + Text("Theme Mode (System, Light, Dark)") + Spacer(modifier = Modifier.height(16.dp)) + Divider() + Spacer(modifier = Modifier.height(16.dp)) + + Text("AI Configuration", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + // Placeholder for AI settings + Text("Provider (Gemini, Pollinations, etc.)") + Spacer(modifier = Modifier.height(16.dp)) + Divider() + Spacer(modifier = Modifier.height(16.dp)) + + Text("Data", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + // Placeholder for Data settings + Text("Clear Chat History") + } +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/theme/Theme.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/theme/Theme.kt index 7961523..4e4d5cb 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/theme/Theme.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/theme/Theme.kt @@ -7,13 +7,17 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -private val CharcoalDark = Color(0xFF121212) -private val CharcoalLight = Color(0xFF1A1A1A) -private val PureWhite = Color(0xFFFFFFFF) -private val OffWhite = Color(0xFFF5F5F5) +val CharcoalDark = Color(0xFF121212) +val CharcoalLight = Color(0xFF1E1E1E) +val PureWhite = Color(0xFFFFFFFF) +val OffWhite = Color(0xFFF5F5F5) + +// Glassmorphic translucent colors +val GlassDarkSurface = Color(0x661E1E1E) // 40% opacity +val GlassLightSurface = Color(0x66F5F5F5) // 40% opacity private val DarkColors = darkColorScheme( - primary = OffWhite, + primary = PureWhite, background = CharcoalDark, surface = CharcoalLight, onPrimary = CharcoalDark, diff --git a/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt b/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt index 223a829..b500929 100644 --- a/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt +++ b/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt @@ -14,13 +14,12 @@ import io.mockk.mockk class AppViewModelFactoryTest { - private val secureStorage = SecureStorageManager() + private val secureStorage = mockk(relaxed = true) private val apiService = FakeGitHubApiService() private lateinit var factory: AppViewModelFactory @Before fun setup() { - secureStorage.clearSecrets() factory = AppViewModelFactory(secureStorage, apiService, mockk()) } diff --git a/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt b/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt index bfe9a55..c1a1fe2 100644 --- a/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt +++ b/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt @@ -37,7 +37,7 @@ class ReviewViewModelTest { @Before fun setup() { - secureStorage = SecureStorageManager() + secureStorage = mockk(relaxed = true) githubApiService = mockk() pollinationsApiService = mockk() viewModel = ReviewViewModel(secureStorage, githubApiService, pollinationsApiService) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..02f9f46 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "free-ai-rock-browser", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "free-ai-rock-browser", + "devDependencies": { + "@playwright/test": "^1.55.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-v4bVfF0dY2iU7lT+vG1t7L4q1U8AqzR6bB09/yW0Zk+A7oWvjYl+wXq5V8j6X8r6m7G7tE5M5fU/0H/x+J1Hxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-y2+7Vn4G1nB9wGf9qFj6w/TfHlB1H8X/Yl2H0M8Qf7+m/b7z/6Z4/x7O/o+Ym/9/g4N4L8O/4q8A8b2Yv2T8Bw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-v6d4G/z+xR4b+p+rP+0Xm3/kS5R4A2i1O/G4k0u7wU/s2/c/3M/Qe2Oq6s/0J8r5mB4M/1g+Q1B4d/8/P9y+Vw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index 8b087b7..c218b5c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "scripts": { "test:browser": "playwright test" }, + "dependencies": { + "@vercel/analytics": "^1.2.2" + }, "devDependencies": { "@playwright/test": "^1.55.0" } diff --git a/settings.gradle.kts b/settings.gradle.kts index ec66b02..19e74af 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://jitpack.io") } } }