diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt
index 92aecf7ee..2566c167f 100644
--- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt
+++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt
@@ -59,7 +59,7 @@ object Server {
requestTimeout = 0 // disabled
}
install(Auth)
- install(Logging) { level = LogLevel.ALL }
+ install(Logging) { level = LogLevel.INFO }
install(ClientContentNegotiation)
}
diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt
index 2fd517f6d..419efc5de 100644
--- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt
+++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt
@@ -58,10 +58,15 @@ fun Routing.assistantRoutes(logger: KLogger) {
val assistantsApi = openAI.assistants
val response =
assistantsApi.listAssistants(configure = { header("OpenAI-Beta", "assistants=v2") })
+
call.respond(HttpStatusCode.OK, response)
+ } catch (e: SerializationException) {
+ val trace = e.stackTraceToString()
+ logger.error { "Serialization error: $trace" }
+ call.respond(HttpStatusCode.BadRequest, "Serialization error: $trace")
} catch (e: Exception) {
val trace = e.stackTraceToString()
- logger.error { "Error listing assistants: $trace" }
+ logger.error { "Error retrieving assistants: $trace" }
call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace")
}
}
@@ -76,7 +81,12 @@ fun Routing.assistantRoutes(logger: KLogger) {
call.respond(HttpStatusCode.BadRequest, "Invalid assistant id")
return@put
}
- val assistant = Assistant(id)
+
+ val token = call.getToken().value
+ val openAI = OpenAI(Config(token = token), logRequests = true)
+ val assistantsApi = openAI.assistants
+ val assistant = Assistant(id, assistantsApi = assistantsApi)
+
val response = assistant.modify(request).get()
logger.info { "Modified assistant: ${response.name} with id: ${response.id}" }
call.respond(HttpStatusCode.OK, response)
@@ -103,7 +113,8 @@ fun Routing.assistantRoutes(logger: KLogger) {
}
val openAI = OpenAI(Config(token = token.value), logRequests = true)
val assistantsApi = openAI.assistants
- val response = assistantsApi.deleteAssistant(id, configure = { header("OpenAI-Beta", "assistants=v2") })
+ val response =
+ assistantsApi.deleteAssistant(id, configure = { header("OpenAI-Beta", "assistants=v2") })
logger.info { "Deleted assistant: with id: ${response.id}" }
call.respond(status = HttpStatusCode.NoContent, response)
} catch (e: Exception) {
diff --git a/server/web/src/utils/api/chatCompletions.ts b/server/web/src/utils/api/chatCompletions.ts
index 56cdba055..adf4fb61c 100644
--- a/server/web/src/utils/api/chatCompletions.ts
+++ b/server/web/src/utils/api/chatCompletions.ts
@@ -8,4 +8,4 @@ export function openai (settings: Settings): OpenAI {
dangerouslyAllowBrowser: true,
apiKey: settings.apiKey, // defaults to process.env["OPENAI_API_KEY"]
});
-}
\ No newline at end of file
+}
diff --git a/server/xefMobile/.kotlin/sessions/kotlin-compiler-859541645610285268.salive b/server/xefMobile/.kotlin/sessions/kotlin-compiler-10822383352460149637.salive
similarity index 100%
rename from server/xefMobile/.kotlin/sessions/kotlin-compiler-859541645610285268.salive
rename to server/xefMobile/.kotlin/sessions/kotlin-compiler-10822383352460149637.salive
diff --git a/server/xefMobile/composeApp/build.gradle.kts b/server/xefMobile/composeApp/build.gradle.kts
index 3b024bef1..39fc77d4e 100644
--- a/server/xefMobile/composeApp/build.gradle.kts
+++ b/server/xefMobile/composeApp/build.gradle.kts
@@ -90,6 +90,11 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3")
implementation("io.ktor:ktor-client-serialization-jvm:2.3.11")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
+ implementation("androidx.compose.runtime:runtime:1.6.7")
+ implementation("io.ktor:ktor-client-cio-jvm:2.3.11")
+ implementation("io.ktor:ktor-client-cio:2.3.11")
+ implementation("io.ktor:ktor-client-core-jvm:2.3.11")
+ implementation("androidx.compose.material3:material3:1.2.1")
}
}
}
diff --git a/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml b/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml
index c6670f1a1..30c168eb5 100644
--- a/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml
+++ b/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml
@@ -1,6 +1,11 @@
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt
index 6278e6be5..025d57305 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt
@@ -5,14 +5,21 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.server.movile.xef.android.ui.viewmodels.AuthViewModel
import com.xef.xefMobile.services.ApiService
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
class MainActivity : ComponentActivity() {
private lateinit var authViewModel: AuthViewModel
+ private lateinit var settingsViewModel: SettingsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
authViewModel = AuthViewModel(this, ApiService())
+ settingsViewModel = SettingsViewModel(this)
- setContent { XefAndroidApp(authViewModel = authViewModel) }
+ authViewModel.logout()
+
+ setContent {
+ XefAndroidApp(authViewModel = authViewModel, settingsViewModel = settingsViewModel)
+ }
}
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt
index 434256b95..7e6ae86ad 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt
@@ -1,7 +1,6 @@
package com.xef.xefMobile
import android.annotation.SuppressLint
-import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -9,6 +8,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -38,6 +40,14 @@ fun MainLayout(
val CustomTextBlue = Color(0xFF0199D7)
val context = LocalContext.current
+ val authToken by authViewModel.authToken.observeAsState()
+
+ LaunchedEffect(authToken) {
+ if (authToken == null) {
+ navController.navigate(Screens.Login.screen) { popUpTo(0) { inclusive = true } }
+ }
+ }
+
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = true,
@@ -178,10 +188,11 @@ fun MainLayout(
)
},
onClick = {
- coroutineScope.launch { drawerState.close() }
- authViewModel.logout()
- Toast.makeText(context, "Logged out", Toast.LENGTH_SHORT).show()
- navController.navigate(Screens.Login.screen) { popUpTo(0) { inclusive = true } }
+ coroutineScope.launch {
+ drawerState.close()
+ authViewModel.logout()
+ navController.navigate(Screens.Login.screen) { popUpTo(0) { inclusive = true } }
+ }
}
)
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt
index 81352d59c..2aa786050 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt
@@ -8,57 +8,94 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import com.server.movile.xef.android.ui.screens.*
+import androidx.navigation.navArgument
+import com.server.movile.xef.android.ui.screens.LoginScreen
+import com.server.movile.xef.android.ui.screens.RegisterScreen
import com.server.movile.xef.android.ui.screens.menu.AssistantScreen
import com.server.movile.xef.android.ui.screens.menu.CreateAssistantScreen
import com.server.movile.xef.android.ui.screens.navigationdrawercompose.HomeScreen
import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel
import com.xef.xefMobile.ui.screens.Screens
+import com.xef.xefMobile.ui.screens.SettingsScreen
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
-fun XefAndroidApp(authViewModel: IAuthViewModel) {
- val navigationController = rememberNavController()
+fun XefAndroidApp(authViewModel: IAuthViewModel, settingsViewModel: SettingsViewModel) {
+ val navController = rememberNavController()
val userName by authViewModel.userName.observeAsState("")
NavHost(
- navController = navigationController,
+ navController = navController,
startDestination = Screens.Login.screen,
modifier = Modifier.padding(top = 16.dp)
) {
- composable(Screens.Login.screen) { LoginScreen(authViewModel, navigationController) }
- composable(Screens.Register.screen) { RegisterScreen(authViewModel, navigationController) }
+ composable(Screens.Login.screen) { LoginScreen(authViewModel, navController) }
+ composable(Screens.Register.screen) { RegisterScreen(authViewModel, navController) }
composable(Screens.Home.screen) {
MainLayout(
- navController = navigationController,
+ navController = navController,
authViewModel = authViewModel,
userName = userName.orEmpty()
) {
- HomeScreen(authViewModel, navigationController)
+ HomeScreen(authViewModel, navController)
}
}
composable(Screens.Assistants.screen) {
MainLayout(
- navController = navigationController,
+ navController = navController,
authViewModel = authViewModel,
userName = userName.orEmpty()
) {
- AssistantScreen(navigationController, authViewModel)
+ AssistantScreen(navController, authViewModel, settingsViewModel)
+ }
+ }
+ composable(
+ route = Screens.CreateAssistantWithArgs.screen,
+ arguments = listOf(navArgument("assistantId") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val assistantId = backStackEntry.arguments?.getString("assistantId")
+ MainLayout(
+ navController = navController,
+ authViewModel = authViewModel,
+ userName = userName.orEmpty()
+ ) {
+ CreateAssistantScreen(
+ navController = navController,
+ authViewModel = authViewModel,
+ settingsViewModel = settingsViewModel,
+ assistantId = assistantId
+ )
}
}
composable(Screens.CreateAssistant.screen) {
MainLayout(
- navController = navigationController,
+ navController = navController,
+ authViewModel = authViewModel,
+ userName = userName.orEmpty()
+ ) {
+ CreateAssistantScreen(
+ navController = navController,
+ authViewModel = authViewModel,
+ settingsViewModel = settingsViewModel,
+ assistantId = null
+ )
+ }
+ }
+ composable(Screens.Settings.screen) {
+ MainLayout(
+ navController = navController,
authViewModel = authViewModel,
userName = userName.orEmpty()
) {
- CreateAssistantScreen(navigationController)
+ SettingsScreen(navController, settingsViewModel)
}
}
- // ... other composable screens ...
+ // Add other composable screens here...
}
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt
index 6a8e331b9..9e95225d5 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt
@@ -1,7 +1,21 @@
package com.xef.xefMobile.model
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-@Serializable data class Assistant(val id: String, val name: String)
+@Serializable
+data class Assistant(
+ val id: String,
+ val name: String,
+ @SerialName("created_at") val createdAt: Long,
+ val description: String?,
+ val model: String,
+ val instructions: String,
+ val tools: List,
+ val temperature: Float,
+ @SerialName("top_p") val topP: Float
+)
+
+@Serializable data class Tool(val type: String)
@Serializable data class AssistantsResponse(val data: List)
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt
index ce92f4db2..43129b2f8 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt
@@ -1,6 +1,8 @@
package com.xef.xefMobile.services
import android.util.Log
+import com.server.movile.xef.android.ui.viewmodels.CreateAssistantRequest
+import com.server.movile.xef.android.ui.viewmodels.ModifyAssistantRequest
import com.xef.xefMobile.model.*
import com.xef.xefMobile.network.client.HttpClientProvider
import io.ktor.client.call.*
@@ -14,7 +16,7 @@ class ApiService {
return try {
HttpClientProvider.client
.post {
- url("http://10.0.2.2:8081/register")
+ url("https://ace-asp-ghastly.ngrok-free.app/register")
contentType(ContentType.Application.Json)
setBody(request)
}
@@ -29,7 +31,7 @@ class ApiService {
return try {
val response: HttpResponse =
HttpClientProvider.client.post {
- url("http://10.0.2.2:8081/login")
+ url("https://ace-asp-ghastly.ngrok-free.app/login")
contentType(ContentType.Application.Json)
setBody(request)
}
@@ -48,18 +50,62 @@ class ApiService {
return try {
val response: HttpResponse =
HttpClientProvider.client.get {
- url("http://10.0.2.2:8081/v1/settings/assistants")
+ url("https://ace-asp-ghastly.ngrok-free.app/v1/settings/assistants")
header(HttpHeaders.Authorization, "Bearer $authToken")
- header("OpenAI-Beta", "assistants=v1")
+ header("OpenAI-Beta", "assistants=v2")
}
val responseBody: String = response.bodyAsText()
Log.d("ApiService", "Assistants response body: $responseBody")
-
response.body()
} catch (e: Exception) {
Log.e("ApiService", "Fetching assistants failed: ${e.message}", e)
throw e
}
}
+
+ suspend fun createAssistant(authToken: String, request: CreateAssistantRequest): HttpResponse {
+ return try {
+ HttpClientProvider.client.post {
+ url("https://ace-asp-ghastly.ngrok-free.app/v1/settings/assistants")
+ contentType(ContentType.Application.Json)
+ header(HttpHeaders.Authorization, "Bearer $authToken")
+ setBody(request)
+ }
+ } catch (e: Exception) {
+ Log.e("ApiService", "Creating assistant failed: ${e.message}", e)
+ throw e
+ }
+ }
+
+ suspend fun deleteAssistant(authToken: String, assistantId: String): HttpResponse {
+ return try {
+ HttpClientProvider.client.delete {
+ url("https://ace-asp-ghastly.ngrok-free.app/v1/settings/assistants/$assistantId")
+ header(HttpHeaders.Authorization, "Bearer $authToken")
+ header("OpenAI-Beta", "assistants=v2")
+ }
+ } catch (e: Exception) {
+ Log.e("ApiService", "Deleting assistant failed: ${e.message}", e)
+ throw e
+ }
+ }
+
+ suspend fun updateAssistant(
+ authToken: String,
+ id: String,
+ request: ModifyAssistantRequest
+ ): HttpResponse {
+ return try {
+ HttpClientProvider.client.put {
+ url("https://ace-asp-ghastly.ngrok-free.app/v1/settings/assistants/$id")
+ contentType(ContentType.Application.Json)
+ header(HttpHeaders.Authorization, "Bearer $authToken")
+ setBody(request)
+ }
+ } catch (e: Exception) {
+ Log.e("ApiService", "Updating assistant failed: ${e.message}", e)
+ throw e
+ }
+ }
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt
index dcf40874a..d577af7e3 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt
@@ -1,5 +1,6 @@
package com.xef.xefMobile.ui.composable
+import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
@@ -16,8 +17,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.isGranted
-import com.google.accompanist.permissions.rememberPermissionState
+import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.server.movile.xef.android.ui.themes.CustomColors
import com.xef.xefMobile.ui.viewmodels.PathViewModel
@@ -26,34 +26,47 @@ import com.xef.xefMobile.ui.viewmodels.PathViewModel
fun FilePickerDialog(
onDismissRequest: () -> Unit,
customColors: CustomColors,
- onFilesSelected: () -> Unit // Callback for when files are selected
+ onFilesSelected: () -> Unit,
+ mimeTypeFilter: String = "*/*",
+ isForCodeInterpreter: Boolean = false
) {
val viewModel: PathViewModel = viewModel()
- val state = viewModel.state
+ val state =
+ if (isForCodeInterpreter) viewModel.codeInterpreterState else viewModel.fileSearchState
val context = LocalContext.current
- val permissionState =
- rememberPermissionState(permission = android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ val permissions =
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ listOf(
+ android.Manifest.permission.READ_MEDIA_IMAGES,
+ android.Manifest.permission.READ_MEDIA_VIDEO,
+ android.Manifest.permission.READ_MEDIA_AUDIO
+ )
+ } else {
+ listOf(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ }
+
+ val permissionState = rememberMultiplePermissionsState(permissions)
var selectedFile by remember { mutableStateOf(null) }
- SideEffect {
- if (!permissionState.status.isGranted) {
- permissionState.launchPermissionRequest()
- }
- }
+ LaunchedEffect(Unit) { permissionState.launchMultiplePermissionRequest() }
val filePickerLauncher =
- rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetMultipleContents(),
- onResult = { uris ->
- viewModel.onFilePathsListChange(uris, context)
- if (uris.isNotEmpty()) {
- onFilesSelected() // Call the callback when files are selected
- selectedFile = state.filePaths.firstOrNull()
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocument()) { uri ->
+ uri?.let {
+ val takeFlags: Int =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ context.contentResolver.takePersistableUriPermission(it, takeFlags)
+ if (isForCodeInterpreter) {
+ viewModel.onCodeInterpreterPathsChange(listOf(it), context)
+ } else {
+ viewModel.onFileSearchPathsChange(listOf(it), context)
}
+ onFilesSelected()
+ selectedFile = state.filePaths.firstOrNull()
}
- )
+ }
AlertDialog(
onDismissRequest = onDismissRequest,
@@ -61,7 +74,7 @@ fun FilePickerDialog(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Selected Files", fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
- HorizontalDivider()
+ Divider()
}
},
text = {
@@ -93,10 +106,10 @@ fun FilePickerDialog(
}
OutlinedButton(
onClick = {
- if (permissionState.status.isGranted) {
- filePickerLauncher.launch("*/*")
+ if (permissionState.allPermissionsGranted) {
+ filePickerLauncher.launch(arrayOf(mimeTypeFilter))
} else {
- permissionState.launchPermissionRequest()
+ permissionState.launchMultiplePermissionRequest()
}
},
colors =
@@ -110,7 +123,11 @@ fun FilePickerDialog(
if (selectedFile != null) {
OutlinedButton(
onClick = {
- viewModel.removeFilePath(selectedFile!!)
+ if (isForCodeInterpreter) {
+ viewModel.removeCodeInterpreterPath(selectedFile!!)
+ } else {
+ viewModel.removeFileSearchPath(selectedFile!!)
+ }
selectedFile = null
},
colors =
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt
index d6eba6bda..f3e70f1a3 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt
@@ -2,12 +2,10 @@ package com.xef.xefMobile.ui.composable
import android.content.ContentUris
import android.content.Context
-import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
-import java.lang.NumberFormatException
class UriPathFinder {
@@ -18,7 +16,7 @@ class UriPathFinder {
isExternalStorageDocument(uri) -> handleExternalStorageDocument(uri)
isDownloadsDocument(uri) -> handleDownloadsDocument(context, uri)
isMediaDocument(uri) -> handleMediaDocument(context, uri)
- else -> null
+ else -> getDataColumn(context, uri, null, null)
}
}
"content".equals(uri.scheme, ignoreCase = true) -> getDataColumn(context, uri, null, null)
@@ -29,37 +27,36 @@ class UriPathFinder {
private fun handleExternalStorageDocument(uri: Uri): String? {
val docId = DocumentsContract.getDocumentId(uri)
- val split = docId.split(":").toTypedArray()
+ val split = docId.split(":")
val type = split[0]
return if ("primary".equals(type, ignoreCase = true)) {
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} else {
- // Handle non-primary volumes (e.g.,
- // "content://com.android.externalstorage.documents/document/primary:...")
- val storageDefinition = System.getenv("SECONDARY_STORAGE")?.split(":")
- storageDefinition?.find { it.contains(type) }?.let { "$it/${split[1]}" }
+ null
}
}
private fun handleDownloadsDocument(context: Context, uri: Uri): String? {
val id = DocumentsContract.getDocumentId(uri)
- return try {
- val contentUri =
- ContentUris.withAppendedId(
- Uri.parse("content://downloads/public_downloads"),
- java.lang.Long.valueOf(id)
- )
- getDataColumn(context, contentUri, null, null)
- } catch (e: NumberFormatException) {
- // Handle the case where the id is not a pure number
- null
+ return if (id.startsWith("raw:")) {
+ id.removePrefix("raw:")
+ } else {
+ try {
+ val contentUri =
+ ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), id.toLong())
+ getDataColumn(context, contentUri, null, null)
+ } catch (e: NumberFormatException) {
+ null
+ }
}
}
private fun handleMediaDocument(context: Context, uri: Uri): String? {
val docId = DocumentsContract.getDocumentId(uri)
- val split = docId.split(":").toTypedArray()
+ val split = docId.split(":")
val type = split[0]
+ val id = split[1]
+
val contentUri: Uri? =
when (type) {
"image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
@@ -67,9 +64,8 @@ class UriPathFinder {
"audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> null
}
- val selection = "_id=?"
- val selectionArgs = arrayOf(split[1])
- return getDataColumn(context, contentUri, selection, selectionArgs)
+
+ return contentUri?.let { getDataColumn(context, it, "_id=?", arrayOf(id)) }
}
private fun getDataColumn(
@@ -78,13 +74,13 @@ class UriPathFinder {
selection: String?,
selectionArgs: Array?
): String? {
- val cursor: Cursor? =
+ val cursor =
uri?.let {
context.contentResolver.query(it, arrayOf("_data"), selection, selectionArgs, null)
}
return cursor?.use {
if (it.moveToFirst()) {
- val columnIndex: Int = it.getColumnIndexOrThrow("_data")
+ val columnIndex = it.getColumnIndexOrThrow("_data")
it.getString(columnIndex)
} else {
null
@@ -92,15 +88,11 @@ class UriPathFinder {
}
}
- private fun isExternalStorageDocument(uri: Uri): Boolean {
- return "com.android.externalstorage.documents" == uri.authority
- }
+ private fun isExternalStorageDocument(uri: Uri) =
+ "com.android.externalstorage.documents" == uri.authority
- private fun isDownloadsDocument(uri: Uri): Boolean {
- return "com.android.providers.downloads.documents" == uri.authority
- }
+ private fun isDownloadsDocument(uri: Uri) =
+ "com.android.providers.downloads.documents" == uri.authority
- private fun isMediaDocument(uri: Uri): Boolean {
- return "com.android.providers.media.documents" == uri.authority
- }
+ private fun isMediaDocument(uri: Uri) = "com.android.providers.media.documents" == uri.authority
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt
index ef4b14032..4c74dff91 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt
@@ -1,18 +1,23 @@
package com.xef.xefMobile.ui.navigation
import androidx.compose.runtime.Composable
+import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
import com.server.movile.xef.android.ui.screens.LoginScreen
import com.server.movile.xef.android.ui.screens.RegisterScreen
import com.server.movile.xef.android.ui.screens.menu.AssistantScreen
import com.server.movile.xef.android.ui.screens.menu.CreateAssistantScreen
+import com.server.movile.xef.android.ui.screens.navigationdrawercompose.HomeScreen
import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel
import com.xef.xefMobile.ui.screens.Screens
+import com.xef.xefMobile.ui.screens.SettingsScreen
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
@Composable
-fun AppNavigator(authViewModel: IAuthViewModel) {
+fun AppNavigator(authViewModel: IAuthViewModel, settingsViewModel: SettingsViewModel) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screens.Login.screen) {
composable(Screens.Login.screen) {
@@ -22,10 +27,37 @@ fun AppNavigator(authViewModel: IAuthViewModel) {
RegisterScreen(authViewModel = authViewModel, navController = navController)
}
composable(Screens.Assistants.screen) {
- AssistantScreen(navController = navController, authViewModel = authViewModel)
+ AssistantScreen(
+ navController = navController,
+ authViewModel = authViewModel,
+ settingsViewModel = settingsViewModel
+ )
+ }
+ composable(
+ route = Screens.CreateAssistantWithArgs.screen,
+ arguments = listOf(navArgument("assistantId") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val assistantId = backStackEntry.arguments?.getString("assistantId")
+ CreateAssistantScreen(
+ navController = navController,
+ authViewModel = authViewModel,
+ settingsViewModel = settingsViewModel,
+ assistantId = assistantId
+ )
+ }
+ composable(Screens.Settings.screen) {
+ SettingsScreen(navController = navController, settingsViewModel = settingsViewModel)
+ }
+ composable(Screens.Home.screen) {
+ HomeScreen(authViewModel = authViewModel, navController = navController)
}
composable(Screens.CreateAssistant.screen) {
- CreateAssistantScreen(navController = navController)
+ CreateAssistantScreen(
+ navController = navController,
+ authViewModel = authViewModel,
+ settingsViewModel = settingsViewModel,
+ assistantId = null
+ )
}
}
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt
index af9fd6fbb..583bce7a3 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt
@@ -23,6 +23,7 @@ import com.xef.xefMobile.ui.screens.Screens
@Composable
fun LoginScreen(authViewModel: IAuthViewModel, navController: NavController) {
val authToken by authViewModel.authToken.observeAsState()
+ val loginError by authViewModel.loginError.observeAsState()
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf(null) }
@@ -52,6 +53,9 @@ fun LoginScreen(authViewModel: IAuthViewModel, navController: NavController) {
if (errorMessage != null) {
Text(text = errorMessage!!, color = Color.Red, textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(8.dp))
+ } else if (loginError != null) {
+ Text(text = loginError!!, color = Color.Red, textAlign = TextAlign.Center)
+ Spacer(modifier = Modifier.height(8.dp))
}
OutlinedTextField(
@@ -75,12 +79,10 @@ fun LoginScreen(authViewModel: IAuthViewModel, navController: NavController) {
onClick = {
when {
email.isBlank() -> {
- errorMessage = "Email field is empty"
- Log.d("LoginScreen", "Email field is empty")
+ errorMessage = "Please enter your email."
}
password.isBlank() -> {
- errorMessage = "Password field is empty"
- Log.d("LoginScreen", "Password field is empty")
+ errorMessage = "Please enter your password."
}
else -> {
errorMessage = null
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt
index d03b674c2..4eee9fd82 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt
@@ -96,14 +96,7 @@ fun RegisterScreen(authViewModel: IAuthViewModel, navController: NavController)
Button(
onClick = {
- errorMessage =
- when {
- name.isBlank() -> "Name is empty"
- email.isBlank() -> "Email is empty"
- password.isEmpty() -> "Password is empty"
- password != rePassword -> "Passwords do not match"
- else -> null
- }
+ errorMessage = validateInputs(name, email, password, rePassword)
if (errorMessage == null) {
authViewModel.register(name, email, password)
}
@@ -122,3 +115,25 @@ fun RegisterScreen(authViewModel: IAuthViewModel, navController: NavController)
}
}
}
+
+fun validateInputs(name: String, email: String, password: String, rePassword: String): String? {
+ if (name.isBlank()) return "Name is empty"
+ if (email.isBlank()) return "Email is empty"
+ if (!isValidEmail(email)) return "Email is not valid"
+ if (password.isEmpty()) return "Password is empty"
+ if (password != rePassword) return "Passwords do not match"
+ if (!isValidPassword(password)) return "Password does not meet criteria"
+
+ return null
+}
+
+fun isValidEmail(email: String): Boolean {
+ val emailPattern = "^[A-Za-z0-9+_.-]+@(.+)$"
+ return email.matches(emailPattern.toRegex())
+}
+
+fun isValidPassword(password: String): Boolean {
+ // Minimum 8 characters, at least one letter and one number
+ val passwordPattern = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$"
+ return password.matches(passwordPattern.toRegex())
+}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt
index 351e12c1a..bc5e915fc 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt
@@ -22,4 +22,8 @@ sealed class Screens(val screen: String) {
object Settings : Screens("settingsScreen")
object CreateAssistant : Screens("createAssistantScreen")
+
+ object CreateAssistantWithArgs : Screens("createAssistantScreen/{assistantId}") {
+ fun createRoute(assistantId: String) = "createAssistantScreen/$assistantId"
+ }
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/SettingsScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/SettingsScreen.kt
new file mode 100644
index 000000000..fea01f951
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/SettingsScreen.kt
@@ -0,0 +1,90 @@
+package com.xef.xefMobile.ui.screens
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
+import kotlinx.coroutines.launch
+import org.xef.xefMobile.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
+ val apiKey by settingsViewModel.apiKey.collectAsState()
+ var apiKeyInput by remember { mutableStateOf(apiKey) }
+ val coroutineScope = rememberCoroutineScope()
+
+ val customColors = Color(0xFFADD8E6)
+ val CustomTextBlue = Color(0xFF0199D7)
+
+ var showSnackbar by remember { mutableStateOf(false) }
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ LaunchedEffect(showSnackbar) {
+ if (showSnackbar) {
+ snackbarHostState.showSnackbar("API key saved successfully")
+ showSnackbar = false
+ navController.navigate(Screens.Home.screen)
+ }
+ }
+
+ Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { innerPadding ->
+ Box(
+ modifier = Modifier.fillMaxSize().background(Color.White).padding(innerPadding).padding(16.dp)
+ ) {
+ Column(
+ modifier = Modifier.align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(text = "Settings", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.Black)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "These are xef-server settings.", fontSize = 16.sp, color = Color.Gray)
+ Spacer(modifier = Modifier.height(16.dp))
+ TextField(
+ value = apiKeyInput,
+ onValueChange = { apiKeyInput = it },
+ label = { Text("OpenAI API key") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(
+ onClick = {
+ coroutineScope.launch {
+ settingsViewModel.setApiKey(apiKeyInput)
+ settingsViewModel.saveApiKey()
+ showSnackbar = true
+ }
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = customColors),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.save_24dp),
+ contentDescription = "save settings icon",
+ modifier = Modifier.size(24.dp),
+ colorFilter = ColorFilter.tint(CustomTextBlue)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "Save Settings", color = CustomTextBlue)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt
index 69352e4a8..fb642c87a 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt
@@ -1,86 +1,129 @@
package com.server.movile.xef.android.ui.screens.menu
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
+import com.server.movile.xef.android.ui.themes.LocalCustomColors
+import com.server.movile.xef.android.ui.viewmodels.AssistantViewModel
import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel
-import com.xef.xefMobile.model.Assistant
-import com.xef.xefMobile.services.ApiService
-import com.xef.xefMobile.theme.theme.LocalCustomColors
import com.xef.xefMobile.ui.screens.Screens
-import kotlinx.coroutines.launch
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
@Composable
-fun AssistantScreen(navController: NavController, authViewModel: IAuthViewModel) {
+fun AssistantScreen(
+ navController: NavController,
+ authViewModel: IAuthViewModel,
+ settingsViewModel: SettingsViewModel
+) {
val customColors = LocalCustomColors.current
- val coroutineScope = rememberCoroutineScope()
- var assistants by remember { mutableStateOf>(emptyList()) }
- var loading by remember { mutableStateOf(true) }
- var errorMessage by remember { mutableStateOf(null) }
+ val viewModel: AssistantViewModel =
+ viewModel(factory = AssistantViewModelFactory(authViewModel, settingsViewModel))
+ val assistants by viewModel.assistants.collectAsState()
+ val loading by viewModel.loading.collectAsState()
+ val errorMessage by viewModel.errorMessage.collectAsState()
- val authToken = authViewModel.authToken.value ?: error("Auth token not found")
-
- LaunchedEffect(Unit) {
- coroutineScope.launch {
- try {
- val response = ApiService().getAssistants(authToken)
- assistants = response.data
- } catch (e: Exception) {
- errorMessage = "Failed to load assistants"
- } finally {
- loading = false
- }
- }
- }
+ val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) }
Box(modifier = Modifier.fillMaxSize()) {
- if (loading) {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- } else if (errorMessage != null) {
- Text(
- text = errorMessage!!,
- color = MaterialTheme.colorScheme.error,
- modifier = Modifier.align(Alignment.Center)
- )
- } else {
- Column(
- modifier = Modifier.align(Alignment.TopCenter).padding(20.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
+ when {
+ loading -> {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ errorMessage != null -> {
Text(
- text = "Assistants",
- fontWeight = FontWeight.Bold,
- fontSize = 24.sp,
+ text = errorMessage ?: "",
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.align(Alignment.Center)
)
- HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
-
- Spacer(modifier = Modifier.height(16.dp))
+ }
+ else -> {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally // Align title to center
+ ) {
+ Text(
+ text = "Assistants",
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+ )
+ HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
- assistants.forEach { assistant ->
- Text(text = assistant.name, fontWeight = FontWeight.Bold)
+ Spacer(modifier = Modifier.height(16.dp))
- Text(text = assistant.id)
- Spacer(modifier = Modifier.height(8.dp))
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(assistants) { assistant ->
+ Column(
+ modifier =
+ Modifier.fillMaxWidth().padding(vertical = 8.dp).clickable {
+ navController.navigate(
+ Screens.CreateAssistantWithArgs.createRoute(assistant.id)
+ )
+ },
+ horizontalAlignment = Alignment.Start // Align items to start
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = assistant.name.ifBlank { "Untitled assistant" },
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp
+ )
+ Text(
+ text = sdf.format(Date(assistant.createdAt * 1000)),
+ fontSize = 14.sp,
+ color = Color.Gray,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+ Text(text = "ID: ${assistant.id}", fontSize = 14.sp)
+ }
+ HorizontalDivider(color = Color.Gray)
+ }
+ }
}
}
+ }
- Button(
- onClick = { navController.navigate(Screens.CreateAssistant.screen) },
- colors =
- ButtonDefaults.buttonColors(
- containerColor = customColors.buttonColor,
- contentColor = MaterialTheme.colorScheme.onPrimary
- ),
- modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp)
- ) {
- Text(text = "Create New Assistant")
- }
+ Button(
+ onClick = { navController.navigate(Screens.CreateAssistant.screen) },
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = customColors.buttonColor,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ ),
+ modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp)
+ ) {
+ Text(text = "Create New Assistant")
+ }
+ }
+}
+
+class AssistantViewModelFactory(
+ private val authViewModel: IAuthViewModel,
+ private val settingsViewModel: SettingsViewModel
+) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(AssistantViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST") return AssistantViewModel(authViewModel, settingsViewModel) as T
}
+ throw IllegalArgumentException("Unknown ViewModel class")
}
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt
index 5e2a74409..31abf0507 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt
@@ -1,37 +1,74 @@
package com.server.movile.xef.android.ui.screens.menu
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
+import com.server.movile.xef.android.ui.themes.CustomColors
import com.server.movile.xef.android.ui.themes.LocalCustomColors
+import com.server.movile.xef.android.ui.viewmodels.AssistantViewModel
+import com.server.movile.xef.android.ui.viewmodels.AuthViewModel
+import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel
+import com.server.movile.xef.android.ui.viewmodels.factory.AuthViewModelFactory
+import com.xef.xefMobile.ui.composable.FilePickerDialog
+import com.xef.xefMobile.ui.screens.Screens
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModelFactory
+import kotlinx.coroutines.launch
+import org.xef.xefMobile.R
class CreateAssistantActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val assistantId = intent.getStringExtra("assistantId")
setContent {
- // Pass the NavController to CreateAssistantScreen
val navController = rememberNavController()
- CreateAssistantScreen(navController)
+
+ val authViewModel: AuthViewModel =
+ viewModel(factory = AuthViewModelFactory(applicationContext))
+ val settingsViewModel: SettingsViewModel =
+ viewModel(factory = SettingsViewModelFactory(applicationContext))
+
+ CreateAssistantScreen(navController, authViewModel, settingsViewModel, assistantId)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun CreateAssistantScreen(navController: NavController) {
+fun CreateAssistantScreen(
+ navController: NavController,
+ authViewModel: IAuthViewModel,
+ settingsViewModel: SettingsViewModel,
+ assistantId: String?
+) {
+ val viewModel: AssistantViewModel =
+ viewModel(factory = AssistantViewModelFactory(authViewModel, settingsViewModel))
+ val snackbarHostState = remember { SnackbarHostState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ val selectedAssistant by viewModel.selectedAssistant.collectAsState(initial = null)
+
var name by remember { mutableStateOf("") }
var instructions by remember { mutableStateOf("") }
var temperature by remember { mutableStateOf(1f) }
@@ -39,85 +76,416 @@ fun CreateAssistantScreen(navController: NavController) {
var fileSearchEnabled by remember { mutableStateOf(false) }
var codeInterpreterEnabled by remember { mutableStateOf(false) }
var model by remember { mutableStateOf("gpt-4-turbo") }
- val list = listOf("gpt-4o", "gpt-4", "gpt-3.5-turbo-16K", "gpt-3.5-turbo-0125", "gpt-3.5-turbo")
+ val list =
+ listOf(
+ "gpt-4o",
+ "gpt-4o-2024-05-13",
+ "gpt-4",
+ "gpt-4-vision-preview",
+ "gpt-4-turbo-preview",
+ "gpt-4-2024-04-09",
+ "gpt-4-turbo",
+ "gpt-4-1106-preview",
+ "gpt-4-0613",
+ "gpt-4-0125-preview",
+ "gpt-4",
+ "gpt-3.5-turbo-16K",
+ "gpt-3.5-turbo-0125",
+ "gpt-3.5-turbo"
+ )
var isExpanded by remember { mutableStateOf(false) }
var selectedText by remember { mutableStateOf(list[0]) }
+ var showFilePicker by remember { mutableStateOf(false) }
+ var showCodeInterpreterPicker by remember { mutableStateOf(false) }
+ var showAllItems by remember { mutableStateOf(false) }
val customColors = LocalCustomColors.current
- Box(modifier = Modifier.fillMaxSize()) {
- Column(
- modifier = Modifier.padding(8.dp).fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(text = "Create Assistant", fontSize = 24.sp, modifier = Modifier.padding(bottom = 16.dp))
+ LaunchedEffect(assistantId) {
+ if (assistantId != null) {
+ Log.d("CreateAssistantScreen", "Loading assistant details for id: $assistantId")
+ viewModel.loadAssistantDetails(assistantId)
+ }
+ }
- TextField(
- value = name,
- onValueChange = { name = it },
- label = { Text("Name") },
- modifier = Modifier.fillMaxWidth()
- )
- Spacer(modifier = Modifier.height(8.dp))
- TextField(
- value = instructions,
- onValueChange = { instructions = it },
- label = { Text("Instructions") },
- modifier = Modifier.fillMaxWidth()
- )
- Spacer(modifier = Modifier.height(8.dp))
- Column(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
+ LaunchedEffect(selectedAssistant) {
+ Log.d("CreateAssistantScreen", "Selected assistant changed: $selectedAssistant")
+ selectedAssistant?.let { assistant ->
+ name = assistant.name
+ instructions = assistant.instructions
+ temperature = assistant.temperature
+ topP = assistant.topP
+ model = assistant.model
+ selectedText = assistant.model
+ fileSearchEnabled = assistant.tools.any { it.type == "file_search" }
+ codeInterpreterEnabled = assistant.tools.any { it.type == "code_interpreter" }
+ }
+ }
+
+ Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }, modifier = Modifier.fillMaxSize()) {
+ paddingValues ->
+ Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
+ LazyColumn(
+ modifier = Modifier.padding(8.dp).fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- ExposedDropdownMenuBox(
- expanded = isExpanded,
- onExpandedChange = { isExpanded = !isExpanded },
- modifier = Modifier.fillMaxWidth()
- ) {
- TextField(
- modifier = Modifier.fillMaxWidth().menuAnchor(),
- value = selectedText,
- onValueChange = {},
- readOnly = true,
- trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) }
+ item {
+ Text(
+ text = if (assistantId == null) "Create Assistant" else "Update Assistant",
+ fontSize = 24.sp,
+ modifier = Modifier.padding(bottom = 16.dp)
)
- ExposedDropdownMenu(expanded = isExpanded, onDismissRequest = { isExpanded = false }) {
- list.forEachIndexed { index, text ->
- DropdownMenuItem(
- text = { Text(text = text) },
+ }
+
+ item {
+ Box(modifier = Modifier.fillMaxWidth()) {
+ OutlinedTextField(
+ value = name,
+ onValueChange = { name = it },
+ label = { Text("Name") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+
+ item { Spacer(modifier = Modifier.height(12.dp)) }
+
+ item {
+ Box(modifier = Modifier.fillMaxWidth()) {
+ OutlinedTextField(
+ value = instructions,
+ onValueChange = { instructions = it },
+ label = { Text("Instructions") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+
+ item { Spacer(modifier = Modifier.height(12.dp)) }
+
+ item {
+ Box(modifier = Modifier.fillMaxWidth()) {
+ ExposedDropdownMenuBox(
+ expanded = isExpanded,
+ onExpandedChange = { isExpanded = !isExpanded },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth().menuAnchor(),
+ value = selectedText,
+ onValueChange = {},
+ readOnly = true,
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) }
+ )
+ ExposedDropdownMenu(
+ expanded = isExpanded,
+ onDismissRequest = { isExpanded = false }
+ ) {
+ val itemsToShow = if (showAllItems) list else list.take(5)
+ itemsToShow.forEachIndexed { index, text ->
+ DropdownMenuItem(
+ text = { Text(text = text) },
+ onClick = {
+ selectedText = list[index]
+ model = list[index]
+ isExpanded = false
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
+ )
+ }
+ if (!showAllItems) {
+ DropdownMenuItem(
+ text = { Text(text = "Show more", color = Color.Red) },
+ onClick = { showAllItems = true },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
+ )
+ }
+ }
+ }
+ }
+ }
+
+ item { Spacer(modifier = Modifier.height(12.dp)) }
+
+ item {
+ ToolsSection(
+ showFilePicker = showFilePicker,
+ onShowFilePickerChange = { showFilePicker = it },
+ fileSearchEnabled = fileSearchEnabled,
+ onFileSearchEnabledChange = { fileSearchEnabled = it },
+ showCodeInterpreterPicker = showCodeInterpreterPicker,
+ onShowCodeInterpreterPickerChange = { showCodeInterpreterPicker = it },
+ codeInterpreterEnabled = codeInterpreterEnabled,
+ onCodeInterpreterEnabledChange = { codeInterpreterEnabled = it },
+ customColors = customColors
+ )
+ }
+
+ item { Spacer(modifier = Modifier.height(12.dp)) }
+
+ item {
+ Column {
+ Text(text = "MODEL CONFIGURATION")
+ HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 4.dp))
+ }
+ }
+
+ item { Spacer(modifier = Modifier.height(12.dp)) }
+
+ item {
+ AssistantFloatField(
+ label = "Temperature",
+ value = temperature,
+ onValueChange = { temperature = it },
+ valueRange = 0f..2f
+ )
+ }
+
+ item {
+ AssistantFloatField(
+ label = "Top P",
+ value = topP,
+ onValueChange = { topP = it },
+ valueRange = 0f..1f
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.weight(1f)) {
+ Button(
+ onClick = { navController.navigateUp() },
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = customColors.buttonColor,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text("Cancel")
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Button(
onClick = {
- selectedText = list[index]
- isExpanded = false
+ coroutineScope.launch {
+ if (assistantId == null) {
+ viewModel.createAssistant(
+ name = name,
+ instructions = instructions,
+ temperature = temperature,
+ topP = topP,
+ model = selectedText,
+ fileSearchEnabled = fileSearchEnabled,
+ codeInterpreterEnabled = codeInterpreterEnabled,
+ onSuccess = {
+ coroutineScope.launch {
+ snackbarHostState.showSnackbar("Assistant created successfully")
+ navController.navigate(Screens.Assistants.screen)
+ }
+ },
+ onError = { errorMessage ->
+ Log.e("CreateAssistantScreen", errorMessage)
+ coroutineScope.launch { snackbarHostState.showSnackbar(errorMessage) }
+ }
+ )
+ } else {
+ viewModel.updateAssistant(
+ id = assistantId,
+ name = name,
+ instructions = instructions,
+ temperature = temperature,
+ topP = topP,
+ model = selectedText,
+ fileSearchEnabled = fileSearchEnabled,
+ codeInterpreterEnabled = codeInterpreterEnabled,
+ onSuccess = {
+ coroutineScope.launch {
+ snackbarHostState.showSnackbar("Assistant updated successfully")
+ navController.navigate(Screens.Assistants.screen)
+ }
+ },
+ onError = { errorMessage ->
+ Log.e("CreateAssistantScreen", errorMessage)
+ coroutineScope.launch { snackbarHostState.showSnackbar(errorMessage) }
+ }
+ )
+ }
+ }
},
- contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
- )
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = customColors.buttonColor,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text(if (assistantId == null) "Create" else "Update")
+ }
+ }
+ if (assistantId != null) {
+ IconButton(
+ onClick = {
+ coroutineScope.launch {
+ viewModel.deleteAssistant(
+ assistantId,
+ onSuccess = {
+ coroutineScope.launch {
+ snackbarHostState.showSnackbar("Assistant deleted successfully")
+ navController.navigate(Screens.Assistants.screen)
+ }
+ },
+ onError = { errorMessage ->
+ Log.e("CreateAssistantScreen", errorMessage)
+ coroutineScope.launch { snackbarHostState.showSnackbar(errorMessage) }
+ }
+ )
+ }
+ },
+ modifier = Modifier.size(48.dp).clip(CircleShape),
+ colors =
+ IconButtonDefaults.iconButtonColors(
+ containerColor = Color.Gray,
+ contentColor = Color.White
+ )
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.delete_24dp),
+ contentDescription = "Delete Assistant",
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
}
}
}
}
- Spacer(modifier = Modifier.height(12.dp))
+ }
+
+ if (showFilePicker) {
+ FilePickerDialog(
+ onDismissRequest = { showFilePicker = false },
+ customColors = customColors,
+ onFilesSelected = { showFilePicker = false },
+ mimeTypeFilter = "*/*",
+ isForCodeInterpreter = false
+ )
+ }
+
+ if (showCodeInterpreterPicker) {
+ FilePickerDialog(
+ onDismissRequest = { showCodeInterpreterPicker = false },
+ customColors = customColors,
+ onFilesSelected = { showCodeInterpreterPicker = false },
+ mimeTypeFilter = "*/*",
+ isForCodeInterpreter = true
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AssistantFloatField(
+ label: String,
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ valueRange: ClosedFloatingPointRange
+) {
+ val customColors = LocalCustomColors.current
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text(text = label, modifier = Modifier.padding(bottom = 2.dp))
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
+ Slider(
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ steps = 200,
+ modifier = Modifier.weight(3f),
+ colors =
+ SliderDefaults.colors(
+ thumbColor = customColors.sliderThumbColor,
+ activeTrackColor = customColors.sliderTrackColor
+ )
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ TextField(
+ value = String.format("%.2f", value),
+ onValueChange = {
+ val newValue = it.toFloatOrNull() ?: 0f
+ onValueChange(newValue)
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.width(60.dp).height(50.dp),
+ textStyle = LocalTextStyle.current.copy(fontSize = 12.sp)
+ )
+ }
+ }
+}
+
+@Composable
+fun ExpandableContent(
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ header: @Composable () -> Unit,
+ content: @Composable () -> Unit
+) {
+ Column {
+ header()
+ if (expanded) {
+ content()
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ToolsSection(
+ showFilePicker: Boolean,
+ onShowFilePickerChange: (Boolean) -> Unit,
+ fileSearchEnabled: Boolean,
+ onFileSearchEnabledChange: (Boolean) -> Unit,
+ showCodeInterpreterPicker: Boolean,
+ onShowCodeInterpreterPickerChange: (Boolean) -> Unit,
+ codeInterpreterEnabled: Boolean,
+ onCodeInterpreterEnabledChange: (Boolean) -> Unit,
+ customColors: CustomColors
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExpandableContent(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ header = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "TOOLS")
-
+ Spacer(modifier = Modifier.weight(1f))
+ IconButton(onClick = { expanded = !expanded }) {
+ Icon(
+ imageVector =
+ if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
+ contentDescription = if (expanded) "Collapse" else "Expand"
+ )
+ }
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 10.dp))
}
+ }
+ ) {
+ Column {
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
- OutlinedButton(
- onClick = { /* handle cancel */},
+ TextButton(
+ onClick = { onShowFilePickerChange(true) },
colors =
ButtonDefaults.outlinedButtonColors(
containerColor = Color.Transparent,
contentColor = customColors.buttonColor
)
) {
- Text("File Search")
+ Text("File Search +")
}
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = fileSearchEnabled,
- onCheckedChange = { fileSearchEnabled = it },
+ onCheckedChange = onFileSearchEnabledChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = customColors.sliderThumbColor,
@@ -127,20 +495,20 @@ fun CreateAssistantScreen(navController: NavController) {
}
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
- OutlinedButton(
- onClick = { /* handle cancel */},
+ TextButton(
+ onClick = { onShowCodeInterpreterPickerChange(true) },
colors =
ButtonDefaults.outlinedButtonColors(
containerColor = Color.Transparent,
contentColor = customColors.buttonColor
)
) {
- Text("Code Interpreter")
+ Text("Code Interpreter +")
}
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = codeInterpreterEnabled,
- onCheckedChange = { codeInterpreterEnabled = it },
+ onCheckedChange = onCodeInterpreterEnabledChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = customColors.sliderThumbColor,
@@ -149,104 +517,6 @@ fun CreateAssistantScreen(navController: NavController) {
)
}
Spacer(modifier = Modifier.height(8.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- OutlinedButton(
- onClick = { /* handle cancel */},
- colors =
- ButtonDefaults.outlinedButtonColors(
- containerColor = Color.Transparent,
- contentColor = customColors.buttonColor
- )
- ) {
- Text("Functions")
- }
- Spacer(modifier = Modifier.weight(1f))
- }
- Spacer(modifier = Modifier.height(8.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(text = "MODEL CONFIGURATION")
-
- HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
- }
- Spacer(modifier = Modifier.width(8.dp))
- AssistantFloatField(
- label = "Temperature",
- value = temperature,
- onValueChange = { temperature = it }
- )
-
- AssistantFloatField(label = "Top P", value = topP, onValueChange = { topP = it })
-
- Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
- Button(
- onClick = { navController.navigateUp() },
- colors =
- ButtonDefaults.buttonColors(
- containerColor = customColors.buttonColor,
- contentColor = MaterialTheme.colorScheme.onPrimary
- )
- ) {
- Text("Cancel")
- }
- Spacer(modifier = Modifier.width(8.dp))
- Button(
- onClick = { /* handle create */},
- colors =
- ButtonDefaults.buttonColors(
- containerColor = customColors.buttonColor,
- contentColor = MaterialTheme.colorScheme.onPrimary
- )
- ) {
- Text("Create")
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun AssistantFloatField(label: String, value: Float, onValueChange: (Float) -> Unit) {
- val customColors = LocalCustomColors.current
- Column(modifier = Modifier.fillMaxWidth()) {
- Text(
- text = label,
- modifier = Modifier.padding(bottom = 2.dp) // Reduce padding for the label
- )
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
- Slider(
- value = value,
- onValueChange = onValueChange,
- valueRange = 0f..2f,
- steps = 100, // This ensures the slider moves in increments of 0.02
- modifier = Modifier.weight(3f),
- colors =
- SliderDefaults.colors(
- thumbColor = customColors.sliderThumbColor,
- activeTrackColor = customColors.sliderTrackColor
- )
- )
- Spacer(
- modifier = Modifier.width(2.dp)
- ) // Add a small spacer between the slider and text field
- TextField(
- value = String.format("%.2f", value),
- onValueChange = {
- val newValue = it.toFloatOrNull() ?: 0f
- onValueChange(newValue)
- },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- modifier = Modifier.width(60.dp).height(50.dp),
- textStyle = LocalTextStyle.current.copy(fontSize = 12.sp) // Optionally adjust text size
- )
}
}
}
-
-@Preview(showBackground = false)
-@Composable
-fun CreateAssistantScreenPreview() {
- // Create a mock NavController for the preview
- val navController = rememberNavController()
- CreateAssistantScreen(navController)
-}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AssistantViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AssistantViewModel.kt
new file mode 100644
index 000000000..0f946a037
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AssistantViewModel.kt
@@ -0,0 +1,213 @@
+package com.server.movile.xef.android.ui.viewmodels
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.xef.xefMobile.model.Assistant
+import com.xef.xefMobile.services.ApiService
+import com.xef.xefMobile.ui.viewmodels.SettingsViewModel
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.util.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+
+class AssistantViewModel(
+ private val authViewModel: IAuthViewModel,
+ private val settingsViewModel: SettingsViewModel
+) : ViewModel() {
+ private val _assistants = MutableStateFlow>(emptyList())
+ val assistants: StateFlow> = _assistants.asStateFlow()
+
+ private val _loading = MutableStateFlow(true)
+ val loading: StateFlow = _loading.asStateFlow()
+
+ private val _errorMessage = MutableStateFlow(null)
+ val errorMessage: StateFlow = _errorMessage.asStateFlow()
+
+ private val _selectedAssistant = MutableStateFlow(null)
+ val selectedAssistant: StateFlow = _selectedAssistant.asStateFlow()
+
+ private val apiService = ApiService()
+
+ init {
+ fetchAssistants()
+ }
+
+ fun fetchAssistants(onComplete: (() -> Unit)? = null) {
+ viewModelScope.launch {
+ _loading.value = true
+ _errorMessage.value = null
+ try {
+ val token = settingsViewModel.apiKey.value ?: throw Exception("API key not found")
+ val response = apiService.getAssistants(token)
+ _assistants.value = response.data
+ _loading.value = false
+ onComplete?.invoke()
+ } catch (e: Exception) {
+ _errorMessage.value = "Failed to load assistants: ${e.message}"
+ _loading.value = false
+ onComplete?.invoke()
+ }
+ }
+ }
+
+ fun loadAssistantDetails(id: String) {
+ fetchAssistants {
+ viewModelScope.launch {
+ val assistant = getAssistantById(id)
+ if (assistant != null) {
+ _selectedAssistant.value = assistant
+ Log.d("AssistantViewModel", "Assistant details loaded: $assistant")
+ } else {
+ Log.d("AssistantViewModel", "Assistant not found with id: $id")
+ }
+ }
+ }
+ }
+
+ private fun getAssistantById(id: String): Assistant? {
+ return assistants.value.find { it.id == id }
+ }
+
+ @OptIn(InternalAPI::class)
+ fun createAssistant(
+ name: String,
+ instructions: String,
+ temperature: Float,
+ topP: Float,
+ model: String,
+ fileSearchEnabled: Boolean,
+ codeInterpreterEnabled: Boolean,
+ onSuccess: () -> Unit,
+ onError: (String) -> Unit
+ ) {
+ viewModelScope.launch {
+ try {
+ val token = settingsViewModel.apiKey.value ?: throw Exception("API key not found")
+ val tools = mutableListOf()
+ if (fileSearchEnabled) tools.add(Tool(type = "file_search"))
+ if (codeInterpreterEnabled) tools.add(Tool(type = "code_interpreter"))
+
+ val request =
+ CreateAssistantRequest(
+ model = model,
+ name = name,
+ description = "This is an example assistant for testing purposes.",
+ instructions = instructions,
+ tools = tools,
+ metadata = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3"),
+ temperature = temperature,
+ top_p = topP
+ )
+
+ val response: HttpResponse =
+ apiService.createAssistant(authToken = token, request = request)
+
+ if (response.status == HttpStatusCode.Created) {
+ fetchAssistants()
+ onSuccess()
+ } else {
+ onError("Failed to create assistant: ${response.status}")
+ }
+ } catch (e: Exception) {
+ onError("Error: ${e.message ?: "Unknown error"}")
+ }
+ }
+ }
+
+ fun deleteAssistant(assistantId: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
+ viewModelScope.launch {
+ try {
+ val token = settingsViewModel.apiKey.value ?: throw Exception("API key not found")
+ val response: HttpResponse =
+ apiService.deleteAssistant(authToken = token, assistantId = assistantId)
+
+ if (response.status == HttpStatusCode.NoContent) {
+ fetchAssistants()
+ onSuccess()
+ } else {
+ onError("Failed to delete assistant: ${response.status}")
+ }
+ } catch (e: Exception) {
+ onError("Error: ${e.message ?: "Unknown error"}")
+ }
+ }
+ }
+
+ @OptIn(InternalAPI::class)
+ fun updateAssistant(
+ id: String,
+ name: String,
+ instructions: String,
+ temperature: Float,
+ topP: Float,
+ model: String,
+ fileSearchEnabled: Boolean,
+ codeInterpreterEnabled: Boolean,
+ onSuccess: () -> Unit,
+ onError: (String) -> Unit
+ ) {
+ viewModelScope.launch {
+ try {
+ val token = settingsViewModel.apiKey.value ?: throw Exception("API key not found")
+ val tools = mutableListOf()
+ if (fileSearchEnabled) tools.add(Tool(type = "file_search"))
+ if (codeInterpreterEnabled) tools.add(Tool(type = "code_interpreter"))
+
+ val request =
+ ModifyAssistantRequest(
+ model = model,
+ name = name,
+ description = "This is an example assistant for testing purposes.",
+ instructions = instructions,
+ tools = tools,
+ metadata = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3"),
+ temperature = temperature,
+ top_p = topP
+ )
+
+ val response: HttpResponse =
+ apiService.updateAssistant(authToken = token, id = id, request = request)
+
+ if (response.status == HttpStatusCode.OK) {
+ fetchAssistants()
+ onSuccess()
+ } else {
+ onError("Failed to update assistant: ${response.status}")
+ }
+ } catch (e: Exception) {
+ onError("Error: ${e.message ?: "Unknown error"}")
+ }
+ }
+ }
+}
+
+@Serializable
+data class CreateAssistantRequest(
+ val model: String,
+ val name: String,
+ val description: String?,
+ val instructions: String,
+ val tools: List,
+ val metadata: Map,
+ val temperature: Float,
+ val top_p: Float
+)
+
+@Serializable
+data class ModifyAssistantRequest(
+ val model: String? = null,
+ val name: String? = null,
+ val description: String? = null,
+ val instructions: String? = null,
+ val tools: List? = null,
+ val metadata: Map? = null,
+ val temperature: Float? = null,
+ val top_p: Float? = null
+)
+
+@Serializable data class Tool(val type: String)
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt
index 1aed4dafc..af15147e9 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt
@@ -19,7 +19,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
-// Extension function to provide DataStore instance
private val Context.dataStore by preferencesDataStore(name = "settings")
class AuthViewModel(context: Context, private val apiService: ApiService) :
@@ -39,6 +38,9 @@ class AuthViewModel(context: Context, private val apiService: ApiService) :
private val _userName = MutableLiveData()
override val userName: LiveData = _userName
+ private val _loginError = MutableLiveData()
+ override val loginError: LiveData = _loginError
+
init {
loadAuthToken()
}
@@ -72,9 +74,10 @@ class AuthViewModel(context: Context, private val apiService: ApiService) :
try {
val loginResponse = apiService.loginUser(loginRequest)
updateAuthToken(loginResponse.authToken)
- updateUserName(loginResponse.user.name) // Extract user's name
+ updateUserName(loginResponse.user.name)
_authToken.value = loginResponse.authToken
_userName.value = loginResponse.user.name
+ _loginError.value = null
} catch (e: Exception) {
handleException(e)
} finally {
@@ -102,7 +105,7 @@ class AuthViewModel(context: Context, private val apiService: ApiService) :
try {
val registerResponse = apiService.registerUser(request)
updateAuthToken(registerResponse.authToken)
- updateUserName(name) // Directly use the name provided during registration
+ updateUserName(name)
_authToken.value = registerResponse.authToken
_userName.value = name
} catch (e: Exception) {
@@ -114,11 +117,19 @@ class AuthViewModel(context: Context, private val apiService: ApiService) :
}
private fun handleException(e: Exception) {
- when (e) {
- is IOException -> _errorMessage.postValue("Network error")
- is HttpException -> _errorMessage.postValue("Unexpected server error: ${e.code()}")
- else -> _errorMessage.postValue("An unexpected error occurred: ${e.message}")
- }
+ val errorMessage =
+ when (e) {
+ is IOException -> "Network error"
+ is HttpException -> {
+ when (e.code()) {
+ 401 -> "Incorrect email or password"
+ 404 -> "Email not registered"
+ else -> "Unexpected server error"
+ }
+ }
+ else -> "An unexpected error occurred"
+ }
+ _loginError.postValue(errorMessage)
}
override fun logout() {
@@ -127,11 +138,11 @@ class AuthViewModel(context: Context, private val apiService: ApiService) :
withContext(Dispatchers.IO) {
dataStore.edit { preferences ->
preferences.remove(stringPreferencesKey("authToken"))
- preferences.remove(stringPreferencesKey("userName")) // Add this line
+ preferences.remove(stringPreferencesKey("userName"))
}
}
_authToken.postValue(null)
- _userName.postValue(null) // Add this line
+ _userName.postValue(null)
_errorMessage.postValue("Logged out successfully")
} catch (e: Exception) {
_errorMessage.postValue("Failed to sign out")
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt
index 5cc84e2e5..86cb53267 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt
@@ -7,6 +7,7 @@ interface IAuthViewModel {
val isLoading: LiveData
val errorMessage: LiveData
val userName: LiveData
+ val loginError: LiveData // Add this line
fun login(email: String, password: String)
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt
index 53fc6d469..64f383b17 100644
--- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt
@@ -13,25 +13,45 @@ import kotlinx.coroutines.launch
class PathViewModel : ViewModel() {
- var state by mutableStateOf(PathScreenState())
+ var fileSearchState by mutableStateOf(PathScreenState())
+ private set
+
+ var codeInterpreterState by mutableStateOf(PathScreenState())
private set
private val uriPathFinder = UriPathFinder()
- fun onFilePathsListChange(list: List, context: Context) {
+ fun onFileSearchPathsChange(list: List, context: Context) {
viewModelScope.launch {
- val updatedList = state.filePaths.toMutableList()
+ val updatedList = fileSearchState.filePaths.toMutableList()
val pathList = changeUriToPath(list, context)
updatedList += pathList
- state = state.copy(filePaths = updatedList)
+ fileSearchState = fileSearchState.copy(filePaths = updatedList)
+ }
+ }
+
+ fun onCodeInterpreterPathsChange(list: List, context: Context) {
+ viewModelScope.launch {
+ val updatedList = codeInterpreterState.filePaths.toMutableList()
+ val pathList = changeUriToPath(list, context)
+ updatedList += pathList
+ codeInterpreterState = codeInterpreterState.copy(filePaths = updatedList)
+ }
+ }
+
+ fun removeFileSearchPath(path: String) {
+ viewModelScope.launch {
+ val updatedList = fileSearchState.filePaths.toMutableList()
+ updatedList.remove(path)
+ fileSearchState = fileSearchState.copy(filePaths = updatedList)
}
}
- fun removeFilePath(path: String) {
+ fun removeCodeInterpreterPath(path: String) {
viewModelScope.launch {
- val updatedList = state.filePaths.toMutableList()
+ val updatedList = codeInterpreterState.filePaths.toMutableList()
updatedList.remove(path)
- state = state.copy(filePaths = updatedList)
+ codeInterpreterState = codeInterpreterState.copy(filePaths = updatedList)
}
}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/SettingsViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/SettingsViewModel.kt
new file mode 100644
index 000000000..10dc0c8be
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/SettingsViewModel.kt
@@ -0,0 +1,24 @@
+package com.xef.xefMobile.ui.viewmodels
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+class SettingsViewModel(context: Context) : ViewModel() {
+ private val preferences: SharedPreferences =
+ context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ private val _apiKey = MutableStateFlow(preferences.getString("api_key", "") ?: "")
+ val apiKey: StateFlow = _apiKey
+
+ fun setApiKey(newApiKey: String) {
+ _apiKey.value = newApiKey
+ }
+
+ fun saveApiKey() {
+ viewModelScope.launch { preferences.edit().putString("api_key", _apiKey.value).apply() }
+ }
+}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/factory/AuthViewModelFactory.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/factory/AuthViewModelFactory.kt
new file mode 100644
index 000000000..ec206ffdb
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/factory/AuthViewModelFactory.kt
@@ -0,0 +1,16 @@
+package com.server.movile.xef.android.ui.viewmodels.factory
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.server.movile.xef.android.ui.viewmodels.AuthViewModel
+import com.xef.xefMobile.services.ApiService
+
+class AuthViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(AuthViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST") return AuthViewModel(context, apiService = ApiService()) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/factory/SettingsViewModelFactory.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/factory/SettingsViewModelFactory.kt
new file mode 100644
index 000000000..a6015417e
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/factory/SettingsViewModelFactory.kt
@@ -0,0 +1,14 @@
+package com.xef.xefMobile.ui.viewmodels
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+
+class SettingsViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST") return SettingsViewModel(context) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/save_24dp.xml b/server/xefMobile/composeApp/src/main/res/drawable/save_24dp.xml
new file mode 100644
index 000000000..6fc751aef
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/save_24dp.xml
@@ -0,0 +1,9 @@
+
+
+