From 47016d47c2e92d31325b274e6f8e3c0dea74040e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 05:58:55 +0000 Subject: [PATCH] Remove Code AI and PR Review options - Removes PR Review and Code AI screens and ViewModels. - Cleans up AppViewModelFactory and MainActivity from dependencies. - Deletes unused files, test files, and data directories. - Removes specific API keys requirements for GitHub and Pollinations as they were tied to removed features. Co-authored-by: SayanthRock <202829406+SayanthRock@users.noreply.github.com> --- .../sayanthrock/freeairock/MainActivity.kt | 261 +----------------- .../freeairock/data/ai/AiCodeAnalyzer.kt | 81 ------ .../freeairock/data/ai/CodeAnalysisState.kt | 8 - .../data/ai/PollinationsApiService.kt | 21 -- .../data/github/GitHubApiService.kt | 37 --- .../freeairock/data/github/GitHubModels.kt | 32 --- .../sayanthrock/freeairock/ui/AppViewModel.kt | 74 ----- .../freeairock/ui/AppViewModelFactory.kt | 14 +- .../freeairock/ui/CodeResultScreen.kt | 187 ------------- .../sayanthrock/freeairock/ui/HomeScaffold.kt | 8 +- .../sayanthrock/freeairock/ui/ReviewScreen.kt | 157 ----------- .../freeairock/ui/ReviewViewModel.kt | 46 --- .../freeairock/ui/navigation/Screen.kt | 12 - .../freeairock/MainDispatcherRule.kt | 24 -- .../freeairock/ui/AppViewModelFactoryTest.kt | 61 ---- .../freeairock/ui/ReviewViewModelTest.kt | 65 ----- 16 files changed, 5 insertions(+), 1083 deletions(-) delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/data/ai/AiCodeAnalyzer.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/data/ai/CodeAnalysisState.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/data/ai/PollinationsApiService.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubApiService.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubModels.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/ui/CodeResultScreen.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt delete mode 100644 app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt delete mode 100644 app/src/test/java/com/sayanthrock/freeairock/MainDispatcherRule.kt delete mode 100644 app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt delete mode 100644 app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt diff --git a/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt b/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt index a592281..24d6c75 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt @@ -4,108 +4,32 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.isSystemInDarkTheme -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.OutlinedButton -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.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider -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.ui.AboutScreen -import com.sayanthrock.freeairock.ui.AppViewModel 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.ImageViewModel import com.sayanthrock.freeairock.ui.ThemeMode import com.sayanthrock.freeairock.ui.theme.FreeAiRockTheme -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory class MainActivity : ComponentActivity() { private val secureStorage by lazy { SecureStorageManager(this) } - private val okHttpClient by lazy { - OkHttpClient.Builder() - .addInterceptor { chain -> - val requestBuilder = chain.request().newBuilder() - val token = secureStorage.getGitHubToken() - - if (!token.isNullOrBlank()) { - requestBuilder.header("Authorization", "Bearer $token") - } - - chain.proceed(requestBuilder.build()) - } - .build() - } - - private val githubApiService by lazy { - Retrofit.Builder() - .baseUrl("https://api.github.com/") - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) - .build() - .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, pollinationsApiService) + AppViewModelFactory(secureStorage) } private val viewModelProvider by lazy { ViewModelProvider(this, viewModelFactory) } - private val appViewModel: AppViewModel by lazy { - viewModelProvider[AppViewModel::class.java] - } - - private val reviewViewModel: ReviewViewModel by lazy { - viewModelProvider[ReviewViewModel::class.java] - } - private val imageViewModel: ImageViewModel by lazy { viewModelProvider[ImageViewModel::class.java] } @@ -123,24 +47,6 @@ class MainActivity : ComponentActivity() { FreeAiRockTheme(darkTheme = darkTheme) { HomeScaffold( - codeContent = { modifier -> - 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) }, @@ -156,166 +62,3 @@ class MainActivity : ComponentActivity() { } } } - -@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 - - 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 - ) - - - - 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 - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = fileUrl, - onValueChange = { fileUrl = it }, - label = { Text("GitHub raw/blob file URL") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - 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)) }, - 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 - ) - } - } - } -} 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 deleted file mode 100644 index 586f2a9..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/data/ai/AiCodeAnalyzer.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.sayanthrock.freeairock.data.ai - -class AiCodeAnalyzer(private val pollinationsApiService: PollinationsApiService) { - - suspend fun analyzeCode(fileName: String, rawCode: String): String { - val safeFileName = fileName.ifBlank { "selected-file" } - val prompt = buildString { - appendLine("You are an expert software developer.") - appendLine("Analyze the file named '$safeFileName'.") - appendLine("Explain its core functionality, architecture role, important methods, risks, and improvement ideas.") - appendLine("Keep the explanation clear for a junior developer.") - appendLine() - appendLine("Code:") - appendLine("```") - appendLine(rawCode.take(MAX_CODE_CHARS)) - appendLine("```") - } - - return generateText(prompt, "No explanation generated.") - } - - suspend fun summarizePullRequest(diffCode: String): String { - val prompt = buildString { - appendLine("You are an expert lead developer reviewing a pull request.") - appendLine("Analyze the following Git diff.") - appendLine("Do not read line-by-line changes back to the user.") - appendLine("Explain the high-level purpose, likely bug fix or feature, architecture impact, risk, and testing notes.") - appendLine("Keep the explanation approachable for a junior developer.") - appendLine() - appendLine("Git diff:") - appendLine("```diff") - appendLine(diffCode.take(MAX_DIFF_CHARS)) - appendLine("```") - } - - return generateText(prompt, "No pull request explanation generated.") - } - - suspend fun summarizePullRequest(owner: String, repo: String, pullNumber: Int, diffText: String): String { - val safeOwner = owner.ifBlank { "owner" } - val safeRepo = repo.ifBlank { "repo" } - val prompt = buildString { - appendLine("You are a senior code reviewer.") - appendLine("Summarize pull request #$pullNumber in $safeOwner/$safeRepo using the Git diff below.") - appendLine("Focus on developer impact, affected files, behavior changes, risks, testing notes, and release notes.") - appendLine("Use clear plain English. Avoid repeating every line of the diff.") - appendLine() - appendLine("Return this structure:") - appendLine("1. Summary") - appendLine("2. Key changes") - appendLine("3. Risk level") - appendLine("4. Testing checklist") - appendLine("5. Suggested release note") - appendLine() - appendLine("Git diff:") - appendLine("```diff") - appendLine(diffText.take(MAX_DIFF_CHARS)) - appendLine("```") - } - - return generateText(prompt, "No pull request summary generated.") - } - - private suspend fun generateText(prompt: String, fallback: String): String { - return try { - val response = pollinationsApiService.generateText( - PollinationsRequest( - messages = listOf(PollinationsMessage(role = "user", content = prompt)) - ) - ) - response.takeIf { it.isNotBlank() } ?: fallback - } catch (error: Exception) { - "AI analysis failed: ${error.localizedMessage ?: "Unknown error"}" - } - } - - companion object { - private const val MAX_CODE_CHARS = 12000 - private const val MAX_DIFF_CHARS = 18000 - } -} diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/ai/CodeAnalysisState.kt b/app/src/main/java/com/sayanthrock/freeairock/data/ai/CodeAnalysisState.kt deleted file mode 100644 index 8be092d..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/data/ai/CodeAnalysisState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sayanthrock.freeairock.data.ai - -sealed interface CodeAnalysisState { - data object Idle : CodeAnalysisState - data object Loading : CodeAnalysisState - data class Success(val result: String) : CodeAnalysisState - data class Error(val message: String) : CodeAnalysisState -} 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 deleted file mode 100644 index 712ca47..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/data/ai/PollinationsApiService.kt +++ /dev/null @@ -1,21 +0,0 @@ -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/github/GitHubApiService.kt b/app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubApiService.kt deleted file mode 100644 index 970616d..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubApiService.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.sayanthrock.freeairock.data.github - -import okhttp3.ResponseBody -import retrofit2.http.GET -import retrofit2.http.Headers -import retrofit2.http.Path -import retrofit2.http.Query -import retrofit2.http.Url - -interface GitHubApiService { - - @GET("user/repos") - suspend fun getMyRepositories( - @Query("sort") sort: String = "updated", - @Query("per_page") perPage: Int = 100 - ): List - - @GET("repos/{owner}/{repo}/contents/{path}") - suspend fun getRepositoryContent( - @Path("owner") owner: String, - @Path("repo") repo: String, - @Path(value = "path", encoded = true) path: String = "" - ): List - - @Headers("Accept: application/vnd.github.v3.diff") - @GET("repos/{owner}/{repo}/pulls/{pullNumber}") - suspend fun getPullRequestDiff( - @Path("owner") owner: String, - @Path("repo") repo: String, - @Path("pullNumber") pullNumber: Int - ): ResponseBody - - @GET - suspend fun downloadRawFile( - @Url fileUrl: String - ): ResponseBody -} diff --git a/app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubModels.kt b/app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubModels.kt deleted file mode 100644 index 7e82f8f..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/data/github/GitHubModels.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.sayanthrock.freeairock.data.github - -import com.google.gson.annotations.SerializedName - -/** - * Represents a GitHub repository returned from GET /user/repos. - */ -data class GitHubRepo( - val id: Long, - val name: String, - @SerializedName("full_name") val fullName: String, - @SerializedName("private") val isPrivate: Boolean, - @SerializedName("html_url") val htmlUrl: String, - val description: String?, - @SerializedName("default_branch") val defaultBranch: String -) - -/** - * Represents a file or directory returned from GET /repos/{owner}/{repo}/contents/{path}. - */ -data class GitHubContentItem( - val name: String, - val path: String, - val sha: String, - val size: Int, - val type: String, - @SerializedName("html_url") val htmlUrl: String?, - @SerializedName("download_url") val downloadUrl: String? -) { - val isFile: Boolean get() = type == "file" - val isDirectory: Boolean get() = type == "dir" -} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt deleted file mode 100644 index 455b620..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.sayanthrock.freeairock.ui - -import androidx.lifecycle.ViewModel -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 -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -class AppViewModel( - private val secureStorage: SecureStorageManager, - private val githubApiService: GitHubApiService, - private val pollinationsApiService: PollinationsApiService -) : ViewModel() { - - private val _analysisState = MutableStateFlow(CodeAnalysisState.Idle) - val analysisState: StateFlow = _analysisState.asStateFlow() - - private var aiAnalyzer: AiCodeAnalyzer? = AiCodeAnalyzer(pollinationsApiService) - - fun refreshAiAnalyzer() { - aiAnalyzer = AiCodeAnalyzer(pollinationsApiService) - } - - fun saveKeys(githubToken: String) { - secureStorage.saveGitHubToken(githubToken) - refreshAiAnalyzer() - } - - fun analyzeCodeFile(fileName: String, downloadUrl: String?) { - 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" - ) - } - } - } - - 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/AppViewModelFactory.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt index e7db795..e543ae5 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/AppViewModelFactory.kt @@ -2,27 +2,15 @@ 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 pollinationsApiService: PollinationsApiService + private val secureStorage: SecureStorageManager ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { return when { - modelClass.isAssignableFrom(ReviewViewModel::class.java) -> { - ReviewViewModel(secureStorage, apiService, pollinationsApiService) as T - } - - modelClass.isAssignableFrom(AppViewModel::class.java) -> { - AppViewModel(secureStorage, apiService, pollinationsApiService) as T - } - modelClass.isAssignableFrom(ImageViewModel::class.java) -> { ImageViewModel(secureStorage) as T } diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/CodeResultScreen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/CodeResultScreen.kt deleted file mode 100644 index 319eb5c..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/CodeResultScreen.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.sayanthrock.freeairock.ui - -import android.content.Context -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -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.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.unit.dp -import com.sayanthrock.freeairock.data.ai.CodeAnalysisState -import java.io.FileOutputStream - -@Composable -fun CodeResultScreen( - fileName: String, - uiState: CodeAnalysisState, - onAnalyzeClick: () -> Unit, - modifier: Modifier = Modifier -) { - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current - var currentAiResult by remember { mutableStateOf("") } - var saveMessage by remember { mutableStateOf(null) } - - val createDocumentLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("text/plain") - ) { uri: Uri? -> - saveMessage = if (uri != null) { - saveTextToFile(context, uri, currentAiResult) - } else { - "Save cancelled" - } - } - - Surface( - modifier = modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column( - modifier = Modifier - .padding(24.dp) - .verticalScroll(rememberScrollState()) - ) { - Text( - text = "FREE-AI-ROCK", - style = MaterialTheme.typography.headlineMedium, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "File: $fileName", - style = MaterialTheme.typography.titleMedium, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onBackground - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = onAnalyzeClick, - enabled = uiState !is CodeAnalysisState.Loading, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text(if (uiState is CodeAnalysisState.Loading) "Analyzing..." else "Summarize Code with AI") - } - - Spacer(modifier = Modifier.height(24.dp)) - - when (uiState) { - CodeAnalysisState.Idle -> { - Text( - text = "Select a GitHub file and run AI analysis.", - color = MaterialTheme.colorScheme.onBackground - ) - } - - CodeAnalysisState.Loading -> { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) - } - - is CodeAnalysisState.Success -> { - currentAiResult = uiState.result - - Surface( - color = MaterialTheme.colorScheme.surface, - shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = currentAiResult, - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyLarge - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedButton( - onClick = { - clipboardManager.setText(AnnotatedString(currentAiResult)) - saveMessage = "Copied" - }, - modifier = Modifier.weight(1f) - ) { - Text("Copy") - } - - Button( - onClick = { - val safeName = fileName.replace(Regex("[^A-Za-z0-9._-]"), "_") - createDocumentLauncher.launch("AI_Analysis_$safeName.txt") - }, - modifier = Modifier.weight(1f) - ) { - Text("Save") - } - } - } - - is CodeAnalysisState.Error -> { - Text( - text = "Error: ${uiState.message}", - color = MaterialTheme.colorScheme.error - ) - } - } - - saveMessage?.let { message -> - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = message, - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } -} - -fun saveTextToFile(context: Context, uri: Uri, content: String): String { - return try { - context.contentResolver.openFileDescriptor(uri, "w")?.use { descriptor -> - FileOutputStream(descriptor.fileDescriptor).use { outputStream -> - outputStream.write(content.toByteArray()) - } - } - "Saved successfully" - } catch (error: Exception) { - "Save failed: ${error.localizedMessage ?: "Unknown error"}" - } -} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/HomeScaffold.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/HomeScaffold.kt index e84e375..b14fd1c 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/HomeScaffold.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/HomeScaffold.kt @@ -22,13 +22,11 @@ import com.sayanthrock.freeairock.ui.navigation.Screen @Composable fun HomeScaffold( - codeContent: @Composable (Modifier) -> Unit, - reviewContent: @Composable (Modifier) -> Unit, studioContent: @Composable (Modifier) -> Unit, aboutContent: @Composable (Modifier) -> Unit ) { val navController = rememberNavController() - val screens = listOf(Screen.CodeAnalyzer, Screen.PullRequestReview, Screen.ImageStudio, Screen.About) + val screens = listOf(Screen.ImageStudio, Screen.About) Scaffold( bottomBar = { @@ -55,11 +53,9 @@ fun HomeScaffold( ) { innerPadding -> NavHost( navController = navController, - startDestination = Screen.CodeAnalyzer.route, + startDestination = Screen.ImageStudio.route, modifier = Modifier.padding(innerPadding) ) { - composable(Screen.CodeAnalyzer.route) { codeContent(Modifier) } - composable(Screen.PullRequestReview.route) { reviewContent(Modifier) } composable(Screen.ImageStudio.route) { studioContent(Modifier) } composable(Screen.About.route) { aboutContent(Modifier) } } diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt deleted file mode 100644 index d63a862..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.sayanthrock.freeairock.ui - -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.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -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.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.sayanthrock.freeairock.data.ai.CodeAnalysisState - -@Composable -fun ReviewScreen( - viewModel: ReviewViewModel, - modifier: Modifier = Modifier -) { - val uiState by viewModel.state.collectAsState() - var ownerText by remember { mutableStateOf("SayanthRock") } - var repoText by remember { mutableStateOf("Root-apk") } - var prNumberText by remember { mutableStateOf("") } - val clipboardManager = LocalClipboardManager.current - - Column( - modifier = modifier - .fillMaxSize() - .padding(24.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "PR Review AI", - style = MaterialTheme.typography.headlineMedium, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.align(Alignment.Start) - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = ownerText, - onValueChange = { ownerText = it }, - label = { Text("Owner") }, - modifier = Modifier.weight(1f), - singleLine = true - ) - - OutlinedTextField( - value = repoText, - onValueChange = { repoText = it }, - label = { Text("Repo") }, - modifier = Modifier.weight(1f), - singleLine = true - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = prNumberText, - onValueChange = { prNumberText = it }, - label = { Text("Pull Request #") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { viewModel.run(ownerText, repoText, prNumberText) }, - modifier = Modifier.fillMaxWidth(), - enabled = uiState !is CodeAnalysisState.Loading && prNumberText.isNotBlank() - ) { - Text(if (uiState is CodeAnalysisState.Loading) "Analyzing Diff..." else "Summarize PR") - } - - Spacer(modifier = Modifier.height(32.dp)) - - when (val state = uiState) { - is CodeAnalysisState.Success -> { - Surface( - color = MaterialTheme.colorScheme.surface, - shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = state.result, - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyMedium - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedButton( - onClick = { clipboardManager.setText(AnnotatedString(state.result)) }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Copy Review") - } - } - - is CodeAnalysisState.Error -> { - Surface( - color = MaterialTheme.colorScheme.errorContainer, - shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = state.message, - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.onErrorContainer, - style = MaterialTheme.typography.bodyMedium - ) - } - } - - CodeAnalysisState.Loading -> { - Text( - text = "Reading pull request diff and creating review...", - color = MaterialTheme.colorScheme.onBackground - ) - } - - CodeAnalysisState.Idle -> Unit - } - } -} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt deleted file mode 100644 index 892c7c2..0000000 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/ReviewViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.sayanthrock.freeairock.ui - -import androidx.lifecycle.ViewModel -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.storage.SecureStorageManager -import com.sayanthrock.freeairock.data.ai.PollinationsApiService -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -class ReviewViewModel( - private val storage: SecureStorageManager, - private val service: GitHubApiService, - private val pollinationsApiService: PollinationsApiService -) : ViewModel() { - - private val _state = MutableStateFlow(CodeAnalysisState.Idle) - val state: StateFlow = _state.asStateFlow() - - private var cachedAnalyzer: AiCodeAnalyzer? = null - - fun run(owner: String, repo: String, numberText: String) { - viewModelScope.launch { - _state.value = CodeAnalysisState.Loading - try { - val number = numberText.trim().toIntOrNull() ?: error("Invalid number") - val diffText = service.getPullRequestDiff(owner.trim(), repo.trim(), number).string() - 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") - } - } - } - - private fun getAnalyzer(): AiCodeAnalyzer { - if (cachedAnalyzer == null) { - cachedAnalyzer = AiCodeAnalyzer(pollinationsApiService) - } - return cachedAnalyzer ?: error("AI analyzer unavailable") - } -} diff --git a/app/src/main/java/com/sayanthrock/freeairock/ui/navigation/Screen.kt b/app/src/main/java/com/sayanthrock/freeairock/ui/navigation/Screen.kt index d579de1..6779195 100644 --- a/app/src/main/java/com/sayanthrock/freeairock/ui/navigation/Screen.kt +++ b/app/src/main/java/com/sayanthrock/freeairock/ui/navigation/Screen.kt @@ -5,18 +5,6 @@ sealed class Screen( val title: String, val shortLabel: String ) { - data object CodeAnalyzer : Screen( - route = "code_analyzer", - title = "Code AI", - shortLabel = "AI" - ) - - data object PullRequestReview : Screen( - route = "pr_review", - title = "PR Review", - shortLabel = "PR" - ) - data object ImageStudio : Screen( route = "image_studio", title = "Image Studio", diff --git a/app/src/test/java/com/sayanthrock/freeairock/MainDispatcherRule.kt b/app/src/test/java/com/sayanthrock/freeairock/MainDispatcherRule.kt deleted file mode 100644 index d802323..0000000 --- a/app/src/test/java/com/sayanthrock/freeairock/MainDispatcherRule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.sayanthrock.freeairock - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -@OptIn(ExperimentalCoroutinesApi::class) -class MainDispatcherRule( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() -) : TestWatcher() { - - override fun starting(description: Description) { - Dispatchers.setMain(testDispatcher) - } - - override fun finished(description: Description) { - Dispatchers.resetMain() - } -} diff --git a/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt b/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt deleted file mode 100644 index 223a829..0000000 --- a/app/src/test/java/com/sayanthrock/freeairock/ui/AppViewModelFactoryTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.sayanthrock.freeairock.ui - -import com.sayanthrock.freeairock.data.github.GitHubApiService -import com.sayanthrock.freeairock.data.ai.PollinationsApiService -import com.sayanthrock.freeairock.data.github.GitHubContentItem -import com.sayanthrock.freeairock.data.github.GitHubRepo -import com.sayanthrock.freeairock.data.storage.SecureStorageManager -import okhttp3.ResponseBody -import okhttp3.ResponseBody.Companion.toResponseBody -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import io.mockk.mockk - -class AppViewModelFactoryTest { - - private val secureStorage = SecureStorageManager() - private val apiService = FakeGitHubApiService() - private lateinit var factory: AppViewModelFactory - - @Before - fun setup() { - secureStorage.clearSecrets() - factory = AppViewModelFactory(secureStorage, apiService, mockk()) - } - - @Test - fun `creates AppViewModel`() { - val viewModel = factory.create(AppViewModel::class.java) - - assertTrue(viewModel is AppViewModel) - } - - @Test - fun `creates ReviewViewModel`() { - val viewModel = factory.create(ReviewViewModel::class.java) - - assertTrue(viewModel is ReviewViewModel) - } - - @Test - fun `creates ImageViewModel`() { - val viewModel = factory.create(ImageViewModel::class.java) - - assertTrue(viewModel is ImageViewModel) - } - - @Test(expected = IllegalArgumentException::class) - fun `throws for unknown ViewModel`() { - factory.create(UnknownViewModel::class.java) - } - - private class UnknownViewModel : androidx.lifecycle.ViewModel() - - private class FakeGitHubApiService : GitHubApiService { - override suspend fun getMyRepositories(sort: String, perPage: Int): List = emptyList() - override suspend fun getRepositoryContent(owner: String, repo: String, path: String): List = emptyList() - override suspend fun getPullRequestDiff(owner: String, repo: String, pullNumber: Int): ResponseBody = "".toResponseBody() - override suspend fun downloadRawFile(fileUrl: String): ResponseBody = "".toResponseBody() - } -} diff --git a/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt b/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt deleted file mode 100644 index bfe9a55..0000000 --- a/app/src/test/java/com/sayanthrock/freeairock/ui/ReviewViewModelTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.sayanthrock.freeairock.ui - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.sayanthrock.freeairock.MainDispatcherRule -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.ai.PollinationsRequest -import com.sayanthrock.freeairock.data.github.GitHubContentItem -import com.sayanthrock.freeairock.data.github.GitHubRepo -import com.sayanthrock.freeairock.data.storage.SecureStorageManager -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import okhttp3.ResponseBody -import okhttp3.ResponseBody.Companion.toResponseBody -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import io.mockk.mockk -import io.mockk.coEvery -import io.mockk.every - -@OptIn(ExperimentalCoroutinesApi::class) -class ReviewViewModelTest { - - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - @get:Rule - val mainDispatcherRule = MainDispatcherRule() - - private lateinit var secureStorage: SecureStorageManager - private lateinit var githubApiService: GitHubApiService - private lateinit var pollinationsApiService: PollinationsApiService - private lateinit var viewModel: ReviewViewModel - - @Before - fun setup() { - secureStorage = SecureStorageManager() - githubApiService = mockk() - pollinationsApiService = mockk() - viewModel = ReviewViewModel(secureStorage, githubApiService, pollinationsApiService) - } - - @Test - fun `missing Gemini key updates state to Error`() = runTest { - viewModel.run("owner", "repo", "12") - assertTrue(viewModel.state.value is CodeAnalysisState.Error) - } - - @Test - fun `invalid pull request number updates state to Error`() = runTest { - - viewModel.run("owner", "repo", "not-a-number") - assertTrue(viewModel.state.value is CodeAnalysisState.Error) - } - - private class FakeGitHubApiService : GitHubApiService { - override suspend fun getMyRepositories(sort: String, perPage: Int): List = emptyList() - override suspend fun getRepositoryContent(owner: String, repo: String, path: String): List = emptyList() - override suspend fun getPullRequestDiff(owner: String, repo: String, pullNumber: Int): ResponseBody = "".toResponseBody() - override suspend fun downloadRawFile(fileUrl: String): ResponseBody = "".toResponseBody() - } -}