Skip to content
Closed
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
11 changes: 11 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Palette's Journal - FREE-AI-ROCK

This journal tracks critical UX/accessibility learnings specific to this application's components and design system.

## 2025-02-14 - Auto-Extraction from Raw URLs on Paste
**Learning:** Users naturally copy the entire browser URL (like a full PR link or a raw code link) rather than manually extracting the owner, repo name, PR number, or filename. Requiring them to manually parse these details is a significant cognitive load and causes friction.
**Action:** Always intercepts text input in repo/PR fields. If a URL format is detected, automatically extract all relevant segments (owner, repo, filename, PR #) and distribute them to the corresponding form fields in a single operation.

## 2025-02-14 - Aesthetic-Consistent Show/Hide Secret Toggles
**Learning:** This app operates under a super-minimalist aesthetic and has zero vector icon dependencies (the bottom navigation uses monospace characters as icons). Adding standard vector icons for password toggle (visibility/visibility-off) would introduce dependency overhead or visual inconsistency.
**Action:** Design custom text-based toggles (e.g., "Show" / "Hide" monospace button) as `trailingIcon` on password inputs. This keeps the design system beautifully uniform, maintains full screen-reader accessibility, and avoids bundling heavy vector drawables.
34 changes: 32 additions & 2 deletions app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ 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.font.FontFamily
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import com.sayanthrock.freeairock.data.ai.CodeAnalysisState
Expand Down Expand Up @@ -157,6 +159,17 @@ class MainActivity : ComponentActivity() {
}
}

fun extractFileNameFromUrl(url: String): String? {
val cleanUrl = url.substringBefore('?').substringBefore('#').trimEnd('/')
if (cleanUrl.contains('/')) {
val lastSegment = cleanUrl.substringAfterLast('/')
if (lastSegment.isNotBlank() && lastSegment.contains('.')) {
return lastSegment
}
}
Comment thread
SayanthRock marked this conversation as resolved.
return null
Comment thread
SayanthRock marked this conversation as resolved.
}

@Composable
private fun CodeAnalyzerScreen(
uiState: CodeAnalysisState,
Expand All @@ -166,6 +179,7 @@ private fun CodeAnalyzerScreen(
modifier: Modifier = Modifier
) {
var githubToken by remember { mutableStateOf("") }
var isTokenVisible by remember { mutableStateOf(false) }

var fileName by remember { mutableStateOf("MainActivity.kt") }
var fileUrl by remember { mutableStateOf("") }
Expand Down Expand Up @@ -201,7 +215,18 @@ private fun CodeAnalyzerScreen(
value = githubToken,
onValueChange = { githubToken = it },
label = { Text("GitHub token") },
visualTransformation = PasswordVisualTransformation(),
visualTransformation = if (isTokenVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
androidx.compose.material3.TextButton(
onClick = { isTokenVisible = !isTokenVisible }
) {
Text(
text = if (isTokenVisible) "Hide" else "Show",
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.primary
)
}
},
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Expand Down Expand Up @@ -250,7 +275,12 @@ private fun CodeAnalyzerScreen(

OutlinedTextField(
value = fileUrl,
onValueChange = { fileUrl = it },
onValueChange = { url ->
fileUrl = url
extractFileNameFromUrl(url)?.let {
fileName = it
}
},
Comment thread
SayanthRock marked this conversation as resolved.
label = { Text("GitHub raw/blob file URL") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
Expand Down
23 changes: 20 additions & 3 deletions app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,18 @@ fun ReviewScreen(

OutlinedTextField(
value = prNumberText,
onValueChange = { prNumberText = it },
label = { Text("Pull Request #") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { input ->
val parsed = parseGitHubPrUrl(input)
if (parsed != null) {
ownerText = parsed.first
repoText = parsed.second
prNumberText = parsed.third
} else {
prNumberText = input
}
},
label = { Text("Pull Request # or full URL") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Expand Down Expand Up @@ -155,3 +164,11 @@ fun ReviewScreen(
}
}
}

fun parseGitHubPrUrl(url: String): Triple<String, String, String>? {
val cleanUrl = url.substringBefore('?').substringBefore('#').trim().trimEnd('/')
val regex = """^(?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+)(?:/.*)?$""".toRegex(RegexOption.IGNORE_CASE)
val matchResult = regex.matchEntire(cleanUrl) ?: return null
val (owner, repo, prNumber) = matchResult.destructured
return Triple(owner, repo, prNumber)
}
69 changes: 69 additions & 0 deletions app/src/test/java/com/sayanthrock/freeairock/ui/PrUrlParserTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.sayanthrock.freeairock.ui

import com.sayanthrock.freeairock.extractFileNameFromUrl
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

class PrUrlParserTest {

@Test
fun testParseGitHubPrUrl_validUrls() {
val testCases = listOf(
"https://github.com/SayanthRock/FREE-AI-ROCK-/pull/12" to Triple("SayanthRock", "FREE-AI-ROCK-", "12"),
"http://github.com/SayanthRock/Root-apk/pull/123" to Triple("SayanthRock", "Root-apk", "123"),
"github.com/SayanthRock/Root-apk/pull/123/" to Triple("SayanthRock", "Root-apk", "123"),
"https://www.github.com/SayanthRock/Root-apk/pull/123/files" to Triple("SayanthRock", "Root-apk", "123"),
"https://github.com/SayanthRock/Root-apk/pull/123?diff=unified" to Triple("SayanthRock", "Root-apk", "123")
)

for ((input, expected) in testCases) {
val result = parseGitHubPrUrl(input)
assertEquals("Failed on input: $input", expected, result)
}
}

@Test
fun testParseGitHubPrUrl_invalidUrls() {
val testCases = listOf(
"https://github.com/SayanthRock/FREE-AI-ROCK-",
"not-a-url",
"https://github.com/SayanthRock/FREE-AI-ROCK-/issues/1",
"https://gitlab.com/SayanthRock/Root-apk/pull/123"
)

for (input in testCases) {
val result = parseGitHubPrUrl(input)
assertNull("Expected null on input: $input", result)
}
}

@Test
fun testExtractFileNameFromUrl_validUrls() {
val testCases = listOf(
"https://github.com/SayanthRock/FREE-AI-ROCK-/blob/main/app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt" to "MainActivity.kt",
"https://raw.githubusercontent.com/SayanthRock/FREE-AI-ROCK-/main/app/build.gradle.kts" to "build.gradle.kts",
"https://github.com/owner/repo/blob/branch/src/App.tsx?someQuery=1" to "App.tsx",
"github.com/owner/repo/blob/branch/src/index.html#anchor" to "index.html"
)

for ((input, expected) in testCases) {
val result = extractFileNameFromUrl(input)
assertEquals("Failed on input: $input", expected, result)
}
}

@Test
fun testExtractFileNameFromUrl_invalidUrls() {
val testCases = listOf(
"https://github.com/SayanthRock/FREE-AI-ROCK-",
"https://github.com/SayanthRock/FREE-AI-ROCK-/tree/main/app",
"not-a-file-url"
)

for (input in testCases) {
val result = extractFileNameFromUrl(input)
assertNull("Expected null on input: $input", result)
}
}
}
Loading