diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 410e37f..18a1ea7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") diff --git a/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt b/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt index cd802c0..a592281 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt @@ -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 @@ -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 { @@ -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, @@ -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( @@ -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(null) } @@ -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() diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiCodeAnalyzer.kt b/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiCodeAnalyzer.kt index 55bcaa0..586f2a9 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiCodeAnalyzer.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiCodeAnalyzer.kt @@ -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" } @@ -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)) + ) ) - response.text ?: fallback + response.takeIf { it.isNotBlank() } ?: fallback } catch (error: Exception) { "AI analysis failed: ${error.localizedMessage ?: "Unknown error"}" } diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/ai/PollinationsApiService.kt b/app/src/main/java/com/sayanthrock/freeairock/data/ai/PollinationsApiService.kt new file mode 100644 index 0000000..712ca47 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/data/ai/PollinationsApiService.kt @@ -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, + val model: String? = null +) + +interface PollinationsApiService { + @POST("/") + suspend fun generateText( + @Body request: PollinationsRequest + ): String +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/storage/SecureStorageManager.kt b/app/src/main/java/com/sayanthrock/freeairock/data/storage/SecureStorageManager.kt index 4541ff7..145ebe5 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/data/storage/SecureStorageManager.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/data/storage/SecureStorageManager.kt @@ -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) { @@ -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() @@ -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" } } 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 80a2926..455b620 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt @@ -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 @@ -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.Idle) val analysisState: StateFlow = _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() } @@ -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) diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt index 8307e58..e7db795 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt @@ -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 create(modelClass: Class): 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) -> { diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/ImageStudioScreen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/ImageStudioScreen.kt new file mode 100644 index 0000000..93571d3 --- /dev/null +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/ImageStudioScreen.kt @@ -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(null) } + var isGenerating by remember { mutableStateOf(false) } + + 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 + }, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/ImageViewModel.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/ImageViewModel.kt index 95d171e..915bb72 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/ImageViewModel.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/ImageViewModel.kt @@ -15,7 +15,7 @@ class ImageViewModel( val uiState: StateFlow = _uiState.asStateFlow() fun refreshRenderer() { - secureStorage.getGeminiKey() + // secureStorage.getGeminiKey() } fun create(prompt: String) { diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt index 5fa68d8..892c7c2 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt @@ -6,6 +6,7 @@ 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.storage.SecureStorageManager +import com.sayanthrock.freeairock.data.ai.PollinationsApiService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -13,23 +14,22 @@ import kotlinx.coroutines.launch class ReviewViewModel( private val storage: SecureStorageManager, - private val service: GitHubApiService + private val service: GitHubApiService, + private val pollinationsApiService: PollinationsApiService ) : ViewModel() { private val _state = MutableStateFlow(CodeAnalysisState.Idle) val state: StateFlow = _state.asStateFlow() - private var cachedGeminiKey: String? = null private var cachedAnalyzer: AiCodeAnalyzer? = null fun run(owner: String, repo: String, numberText: String) { viewModelScope.launch { _state.value = CodeAnalysisState.Loading try { - val token = storage.getGeminiKey() ?: error("Gemini key missing") val number = numberText.trim().toIntOrNull() ?: error("Invalid number") val diffText = service.getPullRequestDiff(owner.trim(), repo.trim(), number).string() - val result = getAnalyzer(token).summarizePullRequest(owner.trim(), repo.trim(), number, diffText) + val result = getAnalyzer().summarizePullRequest(owner.trim(), repo.trim(), number, diffText) _state.value = CodeAnalysisState.Success(result) } catch (error: Exception) { _state.value = CodeAnalysisState.Error(error.localizedMessage ?: "Review failed") @@ -37,12 +37,10 @@ class ReviewViewModel( } } - private fun getAnalyzer(geminiKey: String): AiCodeAnalyzer { - if (cachedAnalyzer == null || cachedGeminiKey != geminiKey) { - cachedGeminiKey = geminiKey - cachedAnalyzer = AiCodeAnalyzer(geminiKey) + private fun getAnalyzer(): AiCodeAnalyzer { + if (cachedAnalyzer == null) { + cachedAnalyzer = AiCodeAnalyzer(pollinationsApiService) } - return cachedAnalyzer ?: error("AI analyzer unavailable") } } diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index da2365d..8439233 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,10 +1,10 @@ - +