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") }
}
}