package com.bricks import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll 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.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application import com.bricks.mp.core.BricksContext import com.bricks.mp.core.BricksHttp import com.bricks.mp.core.BricksWidget import com.bricks.mp.core.RenderWidget import com.bricks.mp.actions.ActionDispatcher import com.bricks.mp.sage.SageClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.launch /** * Sage macOS 桌面客户端入口 * 登录 Sage 服务器 -> 加载 UI -> 渲染 */ fun main() = application { val sageClient = remember { SageClient() } val context = remember { BricksContext() } val http = remember { BricksHttp(context) } val scope = rememberCoroutineScope() var isLoggedIn by remember { mutableStateOf(false) } var currentWidget by remember { mutableStateOf(null) } var isLoading by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } // 初始化上下文 LaunchedEffect(Unit) { context.baseUrl = SageClient.DEFAULT_BASE_URL } Window( onCloseRequest = { sageClient.close() http.close() exitApplication() }, title = if (isLoggedIn) "Sage" else "Sage - Login", state = remember { WindowState( placement = if (isLoggedIn) WindowPlacement.Maximized else WindowPlacement.Floating, width = if (isLoggedIn) 1280.dp else 400.dp, height = if (isLoggedIn) 800.dp else 500.dp ) } ) { if (!isLoggedIn) { // 登录界面 LoginScreen( sageClient = sageClient, onLoginSuccess = { isLoggedIn = true // 登录后加载主 UI scope.launch(Dispatchers.IO) { isLoading = true errorMessage = null // Sage 登录后加载入口 UI val result = sageClient.fetchUi("center.ui") result.fold( onSuccess = { widget -> currentWidget = widget isLoading = false }, onFailure = { e -> errorMessage = "加载主界面失败: ${e.message}" isLoading = false } ) } }, onLoginError = { error -> errorMessage = error } ) } else { // 主应用界面 if (isLoading) { LoadingScreen() } else if (currentWidget != null) { MainAppScreen( rootWidget = currentWidget!!, context = context, http = http, sageClient = sageClient, scope = scope, onLogout = { scope.launch { sageClient.logout() isLoggedIn = false currentWidget = null } }, onNavigate = { path -> scope.launch(Dispatchers.IO) { isLoading = true errorMessage = null val result = sageClient.fetchUi(path) result.fold( onSuccess = { widget -> currentWidget = widget isLoading = false }, onFailure = { e -> errorMessage = "加载失败: ${e.message}" isLoading = false } ) } } ) } else { ErrorScreen( message = errorMessage ?: "无法加载主界面", onRetry = { scope.launch(Dispatchers.IO) { isLoading = true errorMessage = null val result = sageClient.fetchUi("center.ui") result.fold( onSuccess = { widget -> currentWidget = widget isLoading = false }, onFailure = { e -> errorMessage = "加载失败: ${e.message}" isLoading = false } ) } } ) } } // 错误提示 errorMessage?.let { msg -> LaunchedEffect(msg) { // 可以通过 dialog 显示错误 println("[Sage] Error: $msg") } } } } /** * 登录界面 */ @Composable fun LoginScreen( sageClient: SageClient, onLoginSuccess: () -> Unit, onLoginError: (String) -> Unit ) { var username by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } var isLoading by remember { mutableStateOf(false) } var loginError by remember { mutableStateOf(null) } var baseUrl by remember { mutableStateOf(SageClient.DEFAULT_BASE_URL) } val scope = rememberCoroutineScope() Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { // Logo / Title Text( text = "Sage", fontSize = 36.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 8.dp) ) Text( text = "AI 平台", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 32.dp) ) // Server URL OutlinedTextField( value = baseUrl, onValueChange = { baseUrl = it }, label = { Text("服务器地址") }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri) ) // Username OutlinedTextField( value = username, onValueChange = { username = it }, label = { Text("用户名") }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), singleLine = true ) // Password OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("密码") }, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), visualTransformation = PasswordVisualTransformation(), singleLine = true ) // Login button Button( onClick = { scope.launch { isLoading = true loginError = null sageClient.baseUrl = baseUrl val success = sageClient.login(username, password) if (success) { onLoginSuccess() } else { loginError = sageClient.loginError.value ?: "登录失败" onLoginError(loginError!!) } isLoading = false } }, enabled = !isLoading && username.isNotBlank() && password.isNotBlank(), modifier = Modifier.fillMaxWidth().height(48.dp) ) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp) } else { Text("登录", fontSize = 16.sp) } } // Error message loginError?.let { error -> Text( text = error, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(top = 16.dp), fontSize = 14.sp ) } } } } /** * 主应用界面 */ @Composable fun MainAppScreen( rootWidget: BricksWidget, context: BricksContext, http: BricksHttp, sageClient: SageClient, scope: androidx.compose.runtime.CoroutineScope, onLogout: () -> Unit, onNavigate: (String) -> Unit ) { val actionDispatcher = remember { ActionDispatcher( context = context, http = http, sageClient = sageClient, scope = scope ).apply { onWidgetLoaded = { widget -> context.setCurrentWidget(widget) } onMessage = { title, message, isError -> println("[Sage] Message: $title - $message (error: $isError)") } } } // 监听 widget 变化 val currentWidget by context.currentWidget.collectAsState() Scaffold( topBar = { TopAppBar( title = { Text("Sage") }, actions = { TextButton(onClick = onLogout) { Text("退出") } } ) } ) { padding -> Box(modifier = Modifier.padding(padding).fillMaxSize()) { val widgetToRender = currentWidget ?: rootWidget RenderWidget( widget = widgetToRender, actionDispatcher = actionDispatcher, modifier = Modifier.fillMaxSize() ) } } } /** * 加载界面 */ @Composable fun LoadingScreen() { Surface(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator() Spacer(modifier = Modifier.height(16.dp)) Text("加载中...") } } } } /** * 错误界面 */ @Composable fun ErrorScreen( message: String, onRetry: () -> Unit ) { Surface(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("❌", fontSize = 48.sp) Spacer(modifier = Modifier.height(16.dp)) Text(message) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onRetry) { Text("重试") } } } } }