forked from sagnik/Project_Velocity
feat/#28 (#29)
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#29
This commit is contained in:
@@ -4,6 +4,11 @@ plugins {
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
}
|
||||
|
||||
fun Project.gradleStringProperty(name: String, defaultValue: String): String {
|
||||
val raw = (findProperty(name) as String?) ?: defaultValue
|
||||
return "\"${raw.replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.desineuron.velocity.edgephone"
|
||||
compileSdk = 35
|
||||
@@ -16,6 +21,10 @@ android {
|
||||
versionName = "1.0.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
buildConfigField("String", "VELOCITY_BASE_URL", project.gradleStringProperty("VELOCITY_BASE_URL", "https://api.desineuron.in"))
|
||||
buildConfigField("String", "VELOCITY_API_EMAIL", project.gradleStringProperty("VELOCITY_API_EMAIL", ""))
|
||||
buildConfigField("String", "VELOCITY_API_PASSWORD", project.gradleStringProperty("VELOCITY_API_PASSWORD", ""))
|
||||
buildConfigField("String", "VELOCITY_BEARER_TOKEN", project.gradleStringProperty("VELOCITY_BEARER_TOKEN", ""))
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -39,6 +48,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +65,7 @@ dependencies {
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
||||
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="Velocity Edge"
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.desineuron.velocity.edgephone.data
|
||||
|
||||
import com.desineuron.velocity.edgephone.BuildConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URLEncoder
|
||||
import java.net.URL
|
||||
|
||||
data class EdgeLead(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val score: Int,
|
||||
val qualification: String,
|
||||
val unitInterest: String,
|
||||
val budget: String,
|
||||
)
|
||||
|
||||
data class EdgeAlertSnapshot(
|
||||
val pendingInsights: Int,
|
||||
val pendingTranscriptions: Int,
|
||||
val upcomingCalendarEvents24h: Int,
|
||||
)
|
||||
|
||||
data class EdgeCommunicationEvent(
|
||||
val id: String,
|
||||
val leadId: String,
|
||||
val leadName: String,
|
||||
val channel: String,
|
||||
val summary: String,
|
||||
val timestamp: String,
|
||||
val recordingRef: String,
|
||||
)
|
||||
|
||||
data class EdgeMemoryFact(
|
||||
val id: String,
|
||||
val factType: String,
|
||||
val factText: String,
|
||||
val createdAt: String,
|
||||
)
|
||||
|
||||
data class EdgeTranscript(
|
||||
val eventId: String,
|
||||
val status: String,
|
||||
val segmentCount: Int,
|
||||
)
|
||||
|
||||
object VelocityEdgeBackend {
|
||||
private val tokenMutex = Mutex()
|
||||
private var cachedToken: String? = null
|
||||
|
||||
val baseUrl: String = BuildConfig.VELOCITY_BASE_URL.trimEnd('/')
|
||||
val authMode: String
|
||||
get() = when {
|
||||
BuildConfig.VELOCITY_BEARER_TOKEN.isNotBlank() -> "Bearer token"
|
||||
BuildConfig.VELOCITY_API_EMAIL.isNotBlank() && BuildConfig.VELOCITY_API_PASSWORD.isNotBlank() -> "Email/password"
|
||||
else -> "Credentials required"
|
||||
}
|
||||
|
||||
val isConfigured: Boolean
|
||||
get() = BuildConfig.VELOCITY_BEARER_TOKEN.isNotBlank() ||
|
||||
(BuildConfig.VELOCITY_API_EMAIL.isNotBlank() && BuildConfig.VELOCITY_API_PASSWORD.isNotBlank())
|
||||
|
||||
suspend fun fetchLeads(): List<EdgeLead> = withContext(Dispatchers.IO) {
|
||||
val root = getJson("/api/leads")
|
||||
val items = root.optJSONArray("data") ?: JSONArray()
|
||||
buildList {
|
||||
for (index in 0 until items.length()) {
|
||||
val item = items.getJSONObject(index)
|
||||
add(
|
||||
EdgeLead(
|
||||
id = item.optString("id"),
|
||||
name = item.optString("name"),
|
||||
score = item.optInt("score"),
|
||||
qualification = item.optString("qualification"),
|
||||
unitInterest = item.optString("unit_interest"),
|
||||
budget = item.optString("budget"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAlerts(): EdgeAlertSnapshot = withContext(Dispatchers.IO) {
|
||||
val root = getJson("/api/mobile-edge/alerts")
|
||||
EdgeAlertSnapshot(
|
||||
pendingInsights = root.optInt("pending_insights"),
|
||||
pendingTranscriptions = root.optInt("pending_transcriptions"),
|
||||
upcomingCalendarEvents24h = root.optInt("upcoming_calendar_events_24h"),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun fetchEvents(lead: EdgeLead, limit: Int = 4): List<EdgeCommunicationEvent> = withContext(Dispatchers.IO) {
|
||||
val root = getJson("/api/mobile-edge/events?lead_id=${lead.id}&limit=$limit")
|
||||
val items = root.optJSONArray("events") ?: JSONArray()
|
||||
buildList {
|
||||
for (index in 0 until items.length()) {
|
||||
val item = items.getJSONObject(index)
|
||||
add(
|
||||
EdgeCommunicationEvent(
|
||||
id = item.optString("event_id"),
|
||||
leadId = item.optString("lead_id"),
|
||||
leadName = lead.name,
|
||||
channel = item.optString("channel"),
|
||||
summary = item.optString("summary").ifBlank { "No summary available." },
|
||||
timestamp = item.optString("timestamp"),
|
||||
recordingRef = item.optString("recording_ref"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchMemoryFacts(leadId: String): List<EdgeMemoryFact> = withContext(Dispatchers.IO) {
|
||||
val encodedLeadId = URLEncoder.encode(leadId, Charsets.UTF_8.name())
|
||||
val root = getJson("/api/mobile-edge/memory?lead_id=$encodedLeadId&limit=10")
|
||||
val items = root.optJSONArray("facts") ?: JSONArray()
|
||||
buildList {
|
||||
for (index in 0 until items.length()) {
|
||||
val item = items.getJSONObject(index)
|
||||
add(
|
||||
EdgeMemoryFact(
|
||||
id = item.optString("fact_id"),
|
||||
factType = item.optString("fact_type"),
|
||||
factText = item.optString("fact_text"),
|
||||
createdAt = item.optString("created_at"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createNote(leadId: String, noteText: String): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val body = JSONObject()
|
||||
.put("lead_id", leadId)
|
||||
.put("note_text", noteText)
|
||||
.put("fact_type", "custom")
|
||||
request("/api/mobile-edge/notes", "POST", body.toString(), authenticated = true, token = getToken())
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchTranscript(eventId: String): Result<EdgeTranscript> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val root = getJson("/api/mobile-edge/transcripts/$eventId")
|
||||
val job = root.optJSONObject("job") ?: JSONObject()
|
||||
val segments = root.optJSONArray("segments") ?: JSONArray()
|
||||
EdgeTranscript(
|
||||
eventId = eventId,
|
||||
status = job.optString("status").ifBlank { "unknown" },
|
||||
segmentCount = segments.length(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getToken(): String {
|
||||
BuildConfig.VELOCITY_BEARER_TOKEN.takeIf { it.isNotBlank() }?.let { return it }
|
||||
if (cachedToken != null) return cachedToken!!
|
||||
return tokenMutex.withLock {
|
||||
cachedToken?.let { return@withLock it }
|
||||
if (!isConfigured) {
|
||||
throw IllegalStateException("Set VELOCITY_BEARER_TOKEN or VELOCITY_API_EMAIL/VELOCITY_API_PASSWORD in Gradle properties.")
|
||||
}
|
||||
val body = JSONObject()
|
||||
.put("email", BuildConfig.VELOCITY_API_EMAIL)
|
||||
.put("password", BuildConfig.VELOCITY_API_PASSWORD)
|
||||
val json = request("/api/auth/login", "POST", body.toString(), authenticated = false)
|
||||
json.optString("access_token").ifBlank {
|
||||
throw IllegalStateException("Velocity login did not return an access token.")
|
||||
}.also { cachedToken = it }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getJson(path: String): JSONObject {
|
||||
val token = getToken()
|
||||
return request(path, "GET", null, authenticated = true, token = token)
|
||||
}
|
||||
|
||||
private suspend fun request(
|
||||
path: String,
|
||||
method: String,
|
||||
body: String?,
|
||||
authenticated: Boolean,
|
||||
token: String? = null,
|
||||
): JSONObject = withContext(Dispatchers.IO) {
|
||||
val connection = (URL("$baseUrl$path").openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = method
|
||||
connectTimeout = 20_000
|
||||
readTimeout = 20_000
|
||||
setRequestProperty("Accept", "application/json")
|
||||
if (body != null) {
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
}
|
||||
if (authenticated) {
|
||||
setRequestProperty("Authorization", "Bearer ${token.orEmpty()}")
|
||||
}
|
||||
}
|
||||
|
||||
body?.let { payload ->
|
||||
OutputStreamWriter(connection.outputStream).use { writer ->
|
||||
writer.write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
val status = connection.responseCode
|
||||
val stream = if (status in 200..299) connection.inputStream else connection.errorStream
|
||||
val text = stream?.bufferedReader()?.use(BufferedReader::readText).orEmpty()
|
||||
|
||||
if (status !in 200..299) {
|
||||
val detail = runCatching { JSONObject(text).optString("detail") }.getOrNull().orEmpty()
|
||||
throw IllegalStateException(detail.ifBlank { "Velocity request failed with HTTP $status." })
|
||||
}
|
||||
|
||||
JSONObject(text.ifBlank { "{}" })
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,39 @@
|
||||
package com.desineuron.velocity.edgephone.features
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.desineuron.velocity.edgephone.data.EdgeAlertSnapshot
|
||||
import com.desineuron.velocity.edgephone.data.VelocityEdgeBackend
|
||||
|
||||
@Composable
|
||||
fun AlertsScreen(paddingValues: PaddingValues) {
|
||||
val state by produceState<Result<EdgeAlertSnapshot>?>(initialValue = null) {
|
||||
value = runCatching { VelocityEdgeBackend.fetchAlerts() }
|
||||
}
|
||||
|
||||
PhoneScaffold(
|
||||
paddingValues = paddingValues,
|
||||
title = "Alerts",
|
||||
subtitle = "High-urgency nudges for unread responses, callbacks, and showroom events.",
|
||||
actionLabel = "Respond to whale-lead unread thread",
|
||||
)
|
||||
subtitle = "High-urgency nudges from the live mobile-edge backend.",
|
||||
) {
|
||||
when (val result = state) {
|
||||
null -> LoadingEdgeCard("Fetching live alert counts.")
|
||||
else -> {
|
||||
val alerts = result.getOrNull()
|
||||
if (alerts != null) {
|
||||
EdgeCard(title = "Live alert posture") {
|
||||
Text("Pending insights: ${alerts.pendingInsights}", color = Color.White)
|
||||
Text("Pending transcriptions: ${alerts.pendingTranscriptions}", color = Color.White)
|
||||
Text("Upcoming 24h calendar events: ${alerts.upcomingCalendarEvents24h}", color = Color.White)
|
||||
}
|
||||
} else {
|
||||
ErrorEdgeCard(result.exceptionOrNull()?.message ?: "Unable to fetch alerts.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
package com.desineuron.velocity.edgephone.features
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.desineuron.velocity.edgephone.data.EdgeCommunicationEvent
|
||||
import com.desineuron.velocity.edgephone.data.VelocityEdgeBackend
|
||||
|
||||
@Composable
|
||||
fun CommunicationsScreen(paddingValues: PaddingValues) {
|
||||
val state by produceState<Result<List<EdgeCommunicationEvent>>?>(initialValue = null) {
|
||||
value = runCatching {
|
||||
val leads = VelocityEdgeBackend.fetchLeads()
|
||||
leads.sortedByDescending { it.score }.take(3).flatMap { VelocityEdgeBackend.fetchEvents(it, limit = 2) }
|
||||
}
|
||||
}
|
||||
|
||||
PhoneScaffold(
|
||||
paddingValues = paddingValues,
|
||||
title = "Communications",
|
||||
subtitle = "Call, WhatsApp, and operator-import touchpoints on a single edge rail.",
|
||||
actionLabel = "Log a manual note after callback",
|
||||
)
|
||||
subtitle = "Recent live call and messaging events for the current priority leads.",
|
||||
) {
|
||||
when (val result = state) {
|
||||
null -> LoadingEdgeCard("Fetching live communication events.")
|
||||
else -> {
|
||||
val events = result.getOrNull()
|
||||
if (events != null) {
|
||||
EdgeCard(title = "Recent threads") {
|
||||
if (events.isEmpty()) {
|
||||
Text("No live communication events were returned yet.", color = Color(0xFF94A3B8))
|
||||
} else {
|
||||
events.forEach { event ->
|
||||
Text("${event.leadName} · ${event.channel}", color = Color.White)
|
||||
Text(event.summary, color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ErrorEdgeCard(result.exceptionOrNull()?.message ?: "Unable to fetch communications.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,44 @@
|
||||
package com.desineuron.velocity.edgephone.features
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.desineuron.velocity.edgephone.data.EdgeLead
|
||||
import com.desineuron.velocity.edgephone.data.VelocityEdgeBackend
|
||||
|
||||
@Composable
|
||||
fun LeadSummaryScreen(paddingValues: PaddingValues) {
|
||||
val state by produceState<Result<List<EdgeLead>>?>(initialValue = null) {
|
||||
value = runCatching { VelocityEdgeBackend.fetchLeads() }
|
||||
}
|
||||
|
||||
PhoneScaffold(
|
||||
paddingValues = paddingValues,
|
||||
title = "Lead Summary",
|
||||
subtitle = "Compact account memory, qualification, and next-best action.",
|
||||
actionLabel = "Review Mohammed Al-Rashid context",
|
||||
)
|
||||
subtitle = "Compact live account context for the highest-priority leads.",
|
||||
) {
|
||||
when (val result = state) {
|
||||
null -> LoadingEdgeCard("Fetching live leads.")
|
||||
else -> {
|
||||
val leads = result.getOrNull()
|
||||
if (leads != null) {
|
||||
EdgeCard(title = "Top leads") {
|
||||
if (leads.isEmpty()) {
|
||||
Text("No live leads are visible to this operator scope yet.", color = Color(0xFF94A3B8))
|
||||
} else {
|
||||
leads.sortedByDescending { it.score }.take(5).forEach { lead ->
|
||||
Text("${lead.name} · ${lead.score}", color = Color.White)
|
||||
Text("${lead.qualification} · ${lead.unitInterest} · ${lead.budget}", color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ErrorEdgeCard(result.exceptionOrNull()?.message ?: "Unable to fetch lead summaries.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,104 @@
|
||||
package com.desineuron.velocity.edgephone.features
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.desineuron.velocity.edgephone.data.EdgeLead
|
||||
import com.desineuron.velocity.edgephone.data.EdgeMemoryFact
|
||||
import com.desineuron.velocity.edgephone.data.VelocityEdgeBackend
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NotesScreen(paddingValues: PaddingValues) {
|
||||
val leadState by produceState<Result<List<EdgeLead>>?>(initialValue = null) {
|
||||
value = runCatching { VelocityEdgeBackend.fetchLeads() }
|
||||
}
|
||||
var noteText by remember { mutableStateOf("") }
|
||||
var submissionMessage by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
PhoneScaffold(
|
||||
paddingValues = paddingValues,
|
||||
title = "Notes",
|
||||
subtitle = "Fast capture for memory facts, objections, and promised follow-ups.",
|
||||
actionLabel = "Create note with memory extraction hint",
|
||||
)
|
||||
subtitle = "Read live memory facts and write a real quick note when credentials are configured.",
|
||||
) {
|
||||
when (val result = leadState) {
|
||||
null -> LoadingEdgeCard("Fetching live leads for note capture.")
|
||||
else -> {
|
||||
val leads = result.getOrNull()
|
||||
if (leads == null) {
|
||||
ErrorEdgeCard(result.exceptionOrNull()?.message ?: "Unable to prepare note capture.")
|
||||
} else {
|
||||
val lead = leads.sortedByDescending { it.score }.firstOrNull()
|
||||
if (lead == null) {
|
||||
EdgeCard(title = "No lead selected") {
|
||||
Text("No live leads are available yet, so note capture cannot be targeted.", color = Color(0xFF94A3B8))
|
||||
}
|
||||
} else {
|
||||
val factsState by produceState<Result<List<EdgeMemoryFact>>?>(initialValue = null, key1 = lead.id) {
|
||||
value = runCatching { VelocityEdgeBackend.fetchMemoryFacts(lead.id) }
|
||||
}
|
||||
EdgeCard(title = "Lead memory · ${lead.name}") {
|
||||
when (val factsResult = factsState) {
|
||||
null -> Text("Loading memory facts…", color = Color(0xFF94A3B8))
|
||||
else -> {
|
||||
val facts = factsResult.getOrNull()
|
||||
if (facts != null) {
|
||||
if (facts.isEmpty()) {
|
||||
Text("No persisted memory facts yet.", color = Color(0xFF94A3B8))
|
||||
} else {
|
||||
facts.forEach { fact ->
|
||||
Text("${fact.factType}: ${fact.factText}", color = Color.White)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(factsResult.exceptionOrNull()?.message ?: "Unable to fetch memory facts.", color = Color(0xFFFCA5A5))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EdgeCard(title = "Create quick note") {
|
||||
OutlinedTextField(
|
||||
value = noteText,
|
||||
onValueChange = { noteText = it },
|
||||
label = { Text("Operator note") },
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
submissionMessage = VelocityEdgeBackend.createNote(lead.id, noteText.trim())
|
||||
.fold(
|
||||
onSuccess = {
|
||||
noteText = ""
|
||||
"Quick note saved to live mobile-edge memory."
|
||||
},
|
||||
onFailure = { it.message ?: "Unable to save quick note." }
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = noteText.isNotBlank() && VelocityEdgeBackend.isConfigured,
|
||||
) {
|
||||
Text("Save note")
|
||||
}
|
||||
submissionMessage?.let { message ->
|
||||
Text(
|
||||
message,
|
||||
color = if (message.contains("saved", ignoreCase = true)) Color(0xFF34D399) else Color(0xFFFCA5A5),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package com.desineuron.velocity.edgephone.features
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -20,7 +22,7 @@ fun PhoneScaffold(
|
||||
paddingValues: PaddingValues,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
actionLabel: String,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -32,21 +34,38 @@ fun PhoneScaffold(
|
||||
) {
|
||||
Text(title, style = MaterialTheme.typography.headlineSmall, color = Color.White)
|
||||
Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF94A3B8))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF0B1220), RoundedCornerShape(24.dp))
|
||||
.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text("Edge action", style = MaterialTheme.typography.labelLarge, color = Color(0xFF38BDF8))
|
||||
Text(actionLabel, style = MaterialTheme.typography.titleMedium, color = Color.White)
|
||||
Text(
|
||||
"This narrow surface is ready for backend hookup to `/api/mobile-edge` once auth and install registration are connected.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF94A3B8),
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EdgeCard(
|
||||
title: String,
|
||||
body: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF0B1220), RoundedCornerShape(24.dp))
|
||||
.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(title, style = MaterialTheme.typography.labelLarge, color = Color(0xFF38BDF8))
|
||||
body()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadingEdgeCard(message: String) {
|
||||
EdgeCard(title = "Loading") {
|
||||
CircularProgressIndicator(color = Color(0xFF38BDF8))
|
||||
Text(message, style = MaterialTheme.typography.bodySmall, color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorEdgeCard(message: String) {
|
||||
EdgeCard(title = "Live backend error") {
|
||||
Text(message, style = MaterialTheme.typography.bodySmall, color = Color(0xFFFCA5A5))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
package com.desineuron.velocity.edgephone.features
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.desineuron.velocity.edgephone.data.VelocityEdgeBackend
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(paddingValues: PaddingValues) {
|
||||
PhoneScaffold(
|
||||
paddingValues = paddingValues,
|
||||
title = "Settings",
|
||||
subtitle = "Install registration, operator identity, and API connection state.",
|
||||
actionLabel = "Verify surface heartbeat and app version",
|
||||
)
|
||||
subtitle = "Runtime configuration for the production edge phone surface.",
|
||||
) {
|
||||
EdgeCard(title = "Connectivity") {
|
||||
Text("Backend: ${VelocityEdgeBackend.baseUrl}", color = Color.White)
|
||||
Text("Auth mode: ${VelocityEdgeBackend.authMode}", color = Color(0xFF94A3B8))
|
||||
Text(
|
||||
if (VelocityEdgeBackend.isConfigured) "Live credentials configured."
|
||||
else "Credentials missing. Configure Gradle properties before field use.",
|
||||
color = if (VelocityEdgeBackend.isConfigured) Color(0xFF34D399) else Color(0xFFF59E0B),
|
||||
)
|
||||
}
|
||||
EdgeCard(title = "Production notes") {
|
||||
Text(
|
||||
"This phone build reads live mobile-edge routes and does not seed fake alerts, leads, or transcript states.",
|
||||
color = Color(0xFF94A3B8),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
package com.desineuron.velocity.edgephone.features
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.desineuron.velocity.edgephone.data.EdgeTranscript
|
||||
import com.desineuron.velocity.edgephone.data.VelocityEdgeBackend
|
||||
|
||||
@Composable
|
||||
fun TranscriptionsScreen(paddingValues: PaddingValues) {
|
||||
val state by produceState<Result<EdgeTranscript?>?>(initialValue = null) {
|
||||
value = runCatching {
|
||||
val lead = VelocityEdgeBackend.fetchLeads().sortedByDescending { it.score }.firstOrNull()
|
||||
val event = lead?.let { VelocityEdgeBackend.fetchEvents(it, limit = 5).firstOrNull { item -> item.recordingRef.isNotBlank() } }
|
||||
event?.let { VelocityEdgeBackend.fetchTranscript(it.id).getOrThrow() }
|
||||
}
|
||||
}
|
||||
|
||||
PhoneScaffold(
|
||||
paddingValues = paddingValues,
|
||||
title = "Transcriptions",
|
||||
subtitle = "Imported voice artifacts and segment-level summaries for the field operator.",
|
||||
actionLabel = "Review pending recording import",
|
||||
)
|
||||
subtitle = "Transcript job status for the latest recording-backed communication event.",
|
||||
) {
|
||||
when (val result = state) {
|
||||
null -> LoadingEdgeCard("Fetching transcript status.")
|
||||
else -> {
|
||||
if (result.isSuccess) {
|
||||
val transcript = result.getOrNull()
|
||||
EdgeCard(title = "Transcript pipeline") {
|
||||
if (transcript == null) {
|
||||
Text("No live recording-backed event is available yet for transcript review.", color = Color(0xFF94A3B8))
|
||||
} else {
|
||||
Text("Event: ${transcript.eventId}", color = Color.White)
|
||||
Text("Job status: ${transcript.status}", color = Color.White)
|
||||
Text("Segments: ${transcript.segmentCount}", color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ErrorEdgeCard(result.exceptionOrNull()?.message ?: "Unable to fetch transcript status.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user