Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ dependencies {

implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.squareup.retrofit2:converter-scalars:2.11.0")
Comment thread
SayanthRock marked this conversation as resolved.
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

Expand Down
41 changes: 21 additions & 20 deletions app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import com.sayanthrock.freeairock.ui.AppViewModelFactory
import com.sayanthrock.freeairock.ui.HomeScaffold
import com.sayanthrock.freeairock.ui.ImageViewModel
import com.sayanthrock.freeairock.ui.PlaceholderPanel
import com.sayanthrock.freeairock.ui.ImageStudioScreen
import com.sayanthrock.freeairock.data.ai.PollinationsApiService
import retrofit2.converter.scalars.ScalarsConverterFactory
import com.sayanthrock.freeairock.ui.ReviewScreen
import com.sayanthrock.freeairock.ui.ReviewViewModel
import com.sayanthrock.freeairock.ui.ThemeMode
Expand Down Expand Up @@ -76,8 +79,19 @@ class MainActivity : ComponentActivity() {
.create(GitHubApiService::class.java)
}


private val pollinationsApiService by lazy {
Retrofit.Builder()
.baseUrl("https://text.pollinations.ai/")
.client(OkHttpClient.Builder().build())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PollinationsApiService::class.java)
}

private val viewModelFactory by lazy {
AppViewModelFactory(secureStorage, githubApiService)
AppViewModelFactory(secureStorage, githubApiService, pollinationsApiService)
}

private val viewModelProvider by lazy {
Expand Down Expand Up @@ -112,8 +126,8 @@ class MainActivity : ComponentActivity() {
codeContent = { modifier ->
CodeAnalyzerScreen(
uiState = appViewModel.analysisState.collectAsState().value,
onSave = { githubToken, geminiKey ->
appViewModel.saveKeys(githubToken, geminiKey)
onSave = { githubToken ->
appViewModel.saveKeys(githubToken)
imageViewModel.refreshRenderer()
},
onAnalyze = appViewModel::analyzeCodeFile,
Expand All @@ -128,11 +142,7 @@ class MainActivity : ComponentActivity() {
)
},
studioContent = { modifier ->
PlaceholderPanel(
title = "Image Studio",
body = "Image renderer, bitmap state, and gallery save helper are ready. Full creation UI will connect here next.",
modifier = modifier
)
ImageStudioScreen(modifier = modifier)
},
aboutContent = { modifier ->
AboutScreen(
Expand All @@ -150,13 +160,13 @@ class MainActivity : ComponentActivity() {
@Composable
private fun CodeAnalyzerScreen(
uiState: CodeAnalysisState,
onSave: (githubToken: String, geminiKey: String) -> Unit,
onSave: (githubToken: String) -> Unit,
onAnalyze: (fileName: String, downloadUrl: String?) -> Unit,
onReset: () -> Unit,
modifier: Modifier = Modifier
) {
var githubToken by remember { mutableStateOf("") }
var geminiKey by remember { mutableStateOf("") }

var fileName by remember { mutableStateOf("MainActivity.kt") }
var fileUrl by remember { mutableStateOf("") }
var savedMessage by remember { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -196,22 +206,13 @@ private fun CodeAnalyzerScreen(
singleLine = true
)

Spacer(modifier = Modifier.height(12.dp))

OutlinedTextField(
value = geminiKey,
onValueChange = { geminiKey = it },
label = { Text("Gemini key") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)

Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
onSave(githubToken, geminiKey)
onSave(githubToken)
savedMessage = "Saved securely on this device"
},
modifier = Modifier.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
package com.sayanthrock.freeairock.data.ai

import com.google.ai.client.generativeai.GenerativeModel
import com.google.ai.client.generativeai.type.content

class AiCodeAnalyzer(apiKey: String) {

private val model = GenerativeModel(
modelName = "gemini-1.5-flash",
apiKey = apiKey
)
class AiCodeAnalyzer(private val pollinationsApiService: PollinationsApiService) {

suspend fun analyzeCode(fileName: String, rawCode: String): String {
val safeFileName = fileName.ifBlank { "selected-file" }
Expand Down Expand Up @@ -71,10 +63,12 @@ class AiCodeAnalyzer(apiKey: String) {

private suspend fun generateText(prompt: String, fallback: String): String {
return try {
val response = model.generateContent(
content { text(prompt) }
val response = pollinationsApiService.generateText(
PollinationsRequest(
messages = listOf(PollinationsMessage(role = "user", content = prompt))
)
)
Comment thread
SayanthRock marked this conversation as resolved.
Comment thread
SayanthRock marked this conversation as resolved.
response.text ?: fallback
response.takeIf { it.isNotBlank() } ?: fallback
} catch (error: Exception) {
"AI analysis failed: ${error.localizedMessage ?: "Unknown error"}"
}
Comment thread
SayanthRock marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.sayanthrock.freeairock.data.ai

import retrofit2.http.Body
import retrofit2.http.POST

data class PollinationsMessage(
val role: String,
val content: String
)

data class PollinationsRequest(
val messages: List<PollinationsMessage>,
val model: String? = null
)

interface PollinationsApiService {
@POST("/")
suspend fun generateText(
@Body request: PollinationsRequest
): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ class SecureStorageManager(context: Context? = null) {
return readString(KEY_GITHUB_TOKEN)?.takeIf { it.isNotBlank() }
}

fun saveGeminiKey(key: String) {
saveString(KEY_GEMINI_KEY, key.trim())
}

fun getGeminiKey(): String? {
return readString(KEY_GEMINI_KEY)?.takeIf { it.isNotBlank() }
}

fun clearSecrets() {
prefs?.edit()?.clear()?.apply()
if (prefs !== legacyPrefs) {
Expand Down Expand Up @@ -72,7 +64,7 @@ class SecureStorageManager(context: Context? = null) {
return
}

listOf(KEY_GITHUB_TOKEN, KEY_GEMINI_KEY).forEach { key ->
listOf(KEY_GITHUB_TOKEN).forEach { key ->
val legacyValue = oldPrefs.getString(key, null)
if (!legacyValue.isNullOrBlank() && securePrefs.getString(key, null).isNullOrBlank()) {
securePrefs.edit().putString(key, legacyValue).apply()
Expand Down Expand Up @@ -105,6 +97,5 @@ class SecureStorageManager(context: Context? = null) {
private const val PREF_NAME = "free_ai_rock_secure_prefs"
private const val FALLBACK_PREF_NAME = "free_ai_rock_prefs"
private const val KEY_GITHUB_TOKEN = "github_token"
private const val KEY_GEMINI_KEY = "gemini_key"
}
}
13 changes: 7 additions & 6 deletions app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.sayanthrock.freeairock.data.ai.AiCodeAnalyzer
import com.sayanthrock.freeairock.data.ai.CodeAnalysisState
import com.sayanthrock.freeairock.data.github.GitHubApiService
import com.sayanthrock.freeairock.data.ai.PollinationsApiService
import com.sayanthrock.freeairock.data.storage.SecureStorageManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -13,21 +14,21 @@ import kotlinx.coroutines.launch

class AppViewModel(
private val secureStorage: SecureStorageManager,
private val githubApiService: GitHubApiService
private val githubApiService: GitHubApiService,
private val pollinationsApiService: PollinationsApiService
) : ViewModel() {

private val _analysisState = MutableStateFlow<CodeAnalysisState>(CodeAnalysisState.Idle)
val analysisState: StateFlow<CodeAnalysisState> = _analysisState.asStateFlow()

private var aiAnalyzer: AiCodeAnalyzer? = secureStorage.getGeminiKey()?.let(::AiCodeAnalyzer)
private var aiAnalyzer: AiCodeAnalyzer? = AiCodeAnalyzer(pollinationsApiService)

fun refreshAiAnalyzer() {
aiAnalyzer = secureStorage.getGeminiKey()?.let(::AiCodeAnalyzer)
aiAnalyzer = AiCodeAnalyzer(pollinationsApiService)
}

fun saveKeys(githubToken: String, geminiKey: String) {
fun saveKeys(githubToken: String) {
secureStorage.saveGitHubToken(githubToken)
secureStorage.saveGeminiKey(geminiKey)
refreshAiAnalyzer()
}

Expand All @@ -37,7 +38,7 @@ class AppViewModel(

try {
val rawUrl = resolveDownloadUrl(downloadUrl)
val analyzer = aiAnalyzer ?: error("Gemini key missing. Add it in settings first.")
val analyzer = aiAnalyzer ?: error("AI Analyzer not initialized")
val rawCode = githubApiService.downloadRawFile(rawUrl).string()
val result = analyzer.analyzeCode(fileName, rawCode)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ package com.sayanthrock.freeairock.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.sayanthrock.freeairock.data.github.GitHubApiService
import com.sayanthrock.freeairock.data.ai.PollinationsApiService
import com.sayanthrock.freeairock.data.storage.SecureStorageManager

class AppViewModelFactory(
private val secureStorage: SecureStorageManager,
private val apiService: GitHubApiService
private val apiService: GitHubApiService,
private val pollinationsApiService: PollinationsApiService
) : ViewModelProvider.Factory {

@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(ReviewViewModel::class.java) -> {
ReviewViewModel(secureStorage, apiService) as T
ReviewViewModel(secureStorage, apiService, pollinationsApiService) as T
}

modelClass.isAssignableFrom(AppViewModel::class.java) -> {
AppViewModel(secureStorage, apiService) as T
AppViewModel(secureStorage, apiService, pollinationsApiService) as T
}

modelClass.isAssignableFrom(ImageViewModel::class.java) -> {
Expand Down
117 changes: 117 additions & 0 deletions app/src/main/java/com/sayanthrock/freeairock/ui/ImageStudioScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.sayanthrock.freeairock.ui

import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
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.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import com.sayanthrock.freeairock.data.image.BitmapImageState

@Composable
fun ImageStudioScreen(
modifier: Modifier = Modifier
) {
var prompt by remember { mutableStateOf("") }
var imageUrl by remember { mutableStateOf<String?>(null) }
var isGenerating by remember { mutableStateOf(false) }
Comment thread
SayanthRock marked this conversation as resolved.

Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Image Studio",
style = MaterialTheme.typography.headlineMedium,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.align(Alignment.Start)
)

Spacer(modifier = Modifier.height(24.dp))

OutlinedTextField(
value = prompt,
onValueChange = { prompt = it },
label = { Text("Describe the image you want") },
modifier = Modifier.fillMaxWidth(),
singleLine = false,
maxLines = 5
)

Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
if (prompt.isNotBlank()) {
val seed = (0..1000000).random()
imageUrl = "https://image.pollinations.ai/prompt/${Uri.encode(prompt)}?seed=$seed"
isGenerating = true
}
},
modifier = Modifier.fillMaxWidth(),
enabled = prompt.isNotBlank() && !isGenerating
) {
Text(if (isGenerating) "Generating..." else "Generate Image")
}

Spacer(modifier = Modifier.height(32.dp))

imageUrl?.let { url ->
Surface(
color = MaterialTheme.colorScheme.surface,
shape = MaterialTheme.shapes.medium,
modifier = Modifier.fillMaxWidth().height(300.dp)
) {
SubcomposeAsyncImage(
model = url,
contentDescription = "Generated Image",
loading = {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator()
}
},
onSuccess = {
isGenerating = false
},
onError = {
isGenerating = false
},
Comment thread
SayanthRock marked this conversation as resolved.
Comment thread
SayanthRock marked this conversation as resolved.
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ImageViewModel(
val uiState: StateFlow<BitmapImageState> = _uiState.asStateFlow()

fun refreshRenderer() {
secureStorage.getGeminiKey()
// secureStorage.getGeminiKey()
}

fun create(prompt: String) {
Expand Down
Loading
Loading