






















@@ -6,7 +6,9 @@ import ai.openclaw.app.MainViewModel
66import ai.openclaw.app.providerDisplayName
77import ai.openclaw.app.ui.design.ClawEmptyState
88import ai.openclaw.app.ui.design.ClawPanel
9+import ai.openclaw.app.ui.design.ClawPrimaryButton
910import ai.openclaw.app.ui.design.ClawScaffold
11+import ai.openclaw.app.ui.design.ClawSecondaryButton
1012import ai.openclaw.app.ui.design.ClawTheme
1113import androidx.compose.foundation.BorderStroke
1214import androidx.compose.foundation.background
@@ -19,6 +21,7 @@ import androidx.compose.foundation.layout.Spacer
1921import androidx.compose.foundation.layout.fillMaxSize
2022import androidx.compose.foundation.layout.fillMaxWidth
2123import androidx.compose.foundation.layout.height
24+import androidx.compose.foundation.layout.heightIn
2225import androidx.compose.foundation.layout.padding
2326import androidx.compose.foundation.layout.size
2427import androidx.compose.foundation.layout.width
@@ -62,6 +65,7 @@ internal fun V2ProvidersModelsScreen(
6265val errorText by viewModel.modelCatalogErrorText.collectAsState()
6366val providerRows = providerRows(providers = providers, models = models)
6467val modelGroups = sortedModelGroups(models)
68+val setupRows = providerSetupRows(providerRows)
65696670LaunchedEffect(isConnected) {
6771if (isConnected) {
@@ -94,7 +98,26 @@ internal fun V2ProvidersModelsScreen(
9498 }
959996100 item {
97-V2ProviderSectionLabel(title = "Providers")
101+V2ProviderOverviewPanel(
102+ isConnected = isConnected,
103+ providerRows = providerRows,
104+ modelCount = models.size,
105+ onRefresh = viewModel::refreshModelCatalog,
106+ onSetup = onAddProvider,
107+ refreshing = refreshing,
108+ )
109+ }
110+111+ item {
112+V2ProviderSectionLabel(title = "Provider setup")
113+ }
114+115+ item {
116+V2ProviderSetupList(rows = setupRows, onSetup = onAddProvider)
117+ }
118+119+ item {
120+V2ProviderSectionLabel(title = "Connected providers")
98121 }
99122100123 item {
@@ -135,6 +158,13 @@ internal fun V2ProvidersModelsScreen(
135158 }
136159}
137160161+private data class V2ProviderSetupRow(
162+val id: String,
163+val name: String,
164+val subtitle: String,
165+val ready: Boolean,
166+)
167+138168private data class V2ProviderRow(
139169val id: String,
140170val name: String,
@@ -174,6 +204,30 @@ private fun providerRows(
174204return (authRows + missingAuthRows).sortedWith(compareBy(::providerPriority, { it.name.lowercase() }))
175205}
176206207+private fun providerSetupRows(providerRows: List<V2ProviderRow>): List<V2ProviderSetupRow> {
208+val byId = providerRows.associateBy { it.id.trim().lowercase() }
209+return listOf("openai", "anthropic", "google", "openrouter", "ollama").map { id ->
210+val row = byId[id] ?: byId["ollama-local"].takeIf { id == "ollama" }
211+V2ProviderSetupRow(
212+ id = id,
213+ name = providerDisplayName(id),
214+ subtitle = providerSetupSubtitle(id, row),
215+ ready = row?.ready == true,
216+ )
217+ }
218+}
219+220+private fun providerSetupSubtitle(
221+id: String,
222+row: V2ProviderRow?,
223+): String =
224+when {
225+ row?.ready == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Ready"
226+ row != null -> "Finish setup to use ${row.name}"
227+ id == "ollama" -> "Use models running on your network"
228+else -> "Add provider credentials on your Gateway"
229+ }
230+177231internal fun modelProviderReady(status: String): Boolean {
178232val normalized = status.trim().lowercase()
179233return normalized == "ok" || normalized == "ready" || normalized == "healthy" || normalized == "configured"
@@ -221,9 +275,102 @@ private fun V2ProviderList(
221275 }
222276}
223277278+@Composable
279+private fun V2ProviderOverviewPanel(
280+isConnected: Boolean,
281+providerRows: List<V2ProviderRow>,
282+modelCount: Int,
283+refreshing: Boolean,
284+onRefresh: () -> Unit,
285+onSetup: () -> Unit,
286+) {
287+val readyCount = providerRows.count { it.ready }
288+val needsSetupCount = providerRows.count { !it.ready }
289+ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)) {
290+Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
291+Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
292+V2ProviderMetricTile(label = "Ready", value = readyCount.toString(), modifier = Modifier.weight(1f))
293+V2ProviderMetricTile(label = "Models", value = modelCount.toString(), modifier = Modifier.weight(1f))
294+V2ProviderMetricTile(label = "Setup", value = needsSetupCount.toString(), modifier = Modifier.weight(1f))
295+ }
296+Text(
297+ text = if (isConnected) "Choose a provider below, then finish credentials on your Gateway." else "Connect your Gateway before adding model providers.",
298+ style = ClawTheme.type.body,
299+ color = ClawTheme.colors.textMuted,
300+ )
301+Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
302+ClawSecondaryButton(text = if (refreshing) "Refreshing" else "Refresh", onClick = onRefresh, enabled = isConnected && !refreshing, modifier = Modifier.weight(1f))
303+ClawPrimaryButton(text = "Setup Provider", onClick = onSetup, enabled = isConnected, modifier = Modifier.weight(1f))
304+ }
305+ }
306+ }
307+}
308+309+@Composable
310+private fun V2ProviderMetricTile(
311+label: String,
312+value: String,
313+modifier: Modifier = Modifier,
314+) {
315+Surface(
316+ modifier = modifier,
317+ shape = RoundedCornerShape(ClawTheme.radii.panel),
318+ color = ClawTheme.colors.surface,
319+ border = BorderStroke(1.dp, ClawTheme.colors.border),
320+ contentColor = ClawTheme.colors.text,
321+ ) {
322+Column(modifier = Modifier.padding(horizontal = 9.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
323+Text(text = value, style = ClawTheme.type.title, color = ClawTheme.colors.text, maxLines = 1)
324+Text(text = label, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
325+ }
326+ }
327+}
328+329+@Composable
330+private fun V2ProviderSetupList(
331+rows: List<V2ProviderSetupRow>,
332+onSetup: () -> Unit,
333+) {
334+ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
335+Column {
336+ rows.forEachIndexed { index, row ->
337+V2ProviderSetupListRow(row = row, onClick = onSetup)
338+if (index != rows.lastIndex) {
339+HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
340+ }
341+ }
342+ }
343+ }
344+}
345+346+@Composable
347+private fun V2ProviderSetupListRow(
348+row: V2ProviderSetupRow,
349+onClick: () -> Unit,
350+) {
351+Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
352+Row(
353+ modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(horizontal = 10.dp, vertical = 6.dp),
354+ verticalAlignment = Alignment.CenterVertically,
355+ horizontalArrangement = Arrangement.spacedBy(10.dp),
356+ ) {
357+V2ProviderBadge(text = row.name)
358+Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
359+Text(text = row.name, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
360+Text(text = row.subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
361+ }
362+Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
363+Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
364+Text(text = if (row.ready) "Ready" else "Setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
365+Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(17.dp), tint = ClawTheme.colors.text)
366+ }
367+ }
368+ }
369+}
370+224371@Composable
225372private fun V2ProviderListRow(row: V2ProviderRow) {
226-Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
373+Row(modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
227374V2ProviderBadge(text = row.name)
228375Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
229376Text(text = row.name, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
@@ -232,16 +379,16 @@ private fun V2ProviderListRow(row: V2ProviderRow) {
232379Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
233380Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
234381Text(text = row.status, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
235-Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(14.dp), tint = ClawTheme.colors.text)
382+Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(17.dp), tint = ClawTheme.colors.text)
236383 }
237384 }
238385}
239386240387@Composable
241388private fun V2ProviderBadge(text: String) {
242-Surface(modifier = Modifier.size(24.dp), shape = RoundedCornerShape(6.dp), color = ClawTheme.colors.surfacePressed, border = BorderStroke(1.dp, ClawTheme.colors.border)) {
389+Surface(modifier = Modifier.size(30.dp), shape = RoundedCornerShape(ClawTheme.radii.row), color = ClawTheme.colors.surfacePressed, border = BorderStroke(1.dp, ClawTheme.colors.border)) {
243390Box(contentAlignment = Alignment.Center) {
244-Text(text = providerInitials(text), style = ClawTheme.type.section, color = ClawTheme.colors.text, textAlign = TextAlign.Center)
391+Text(text = providerInitials(text), style = ClawTheme.type.label, color = ClawTheme.colors.text, textAlign = TextAlign.Center)
245392 }
246393 }
247394}
@@ -275,7 +422,7 @@ private fun V2ModelGroup(
275422) {
276423ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
277424Column {
278-Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
425+Row(modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp).padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
279426V2ProviderBadge(text = providerDisplayName(provider))
280427Text(text = providerDisplayName(provider), style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
281428V2ProviderMiniTag(text = "${models.size} models")
@@ -298,7 +445,7 @@ private fun V2ModelGroup(
298445299446@Composable
300447private fun V2ModelRow(model: GatewayModelSummary) {
301-Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
448+Row(modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp).padding(horizontal = 10.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
302449Text(text = model.name, style = ClawTheme.type.mono, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
303450 modelCapabilityLabels(model).take(3).forEach { label ->
304451V2ProviderMiniTag(text = label)
@@ -333,14 +480,14 @@ private fun V2ProviderHeaderIconButton(
333480) {
334481Surface(
335482 onClick = onClick,
336- modifier = Modifier.size(if (outlined) 28.dp else 30.dp),
483+ modifier = Modifier.size(ClawTheme.spacing.touchTarget),
337484 shape = CircleShape,
338485 color = Color.Transparent,
339486 contentColor = ClawTheme.colors.text,
340487 border = if (outlined) BorderStroke(1.dp, ClawTheme.colors.borderStrong) else null,
341488 ) {
342489Box(contentAlignment = Alignment.Center) {
343-Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(if (outlined) 15.dp else 19.dp))
490+Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(if (outlined) 17.dp else 20.dp))
344491 }
345492 }
346493}
@@ -352,7 +499,7 @@ private fun V2ProviderAddButton(
352499) {
353500Surface(
354501 onClick = onClick,
355- modifier = modifier.fillMaxWidth().height(30.dp),
502+ modifier = modifier.fillMaxWidth().height(ClawTheme.spacing.touchTarget),
356503 shape = RoundedCornerShape(ClawTheme.radii.pill),
357504 color = ClawTheme.colors.primary,
358505 contentColor = ClawTheme.colors.primaryText,
@@ -362,9 +509,9 @@ private fun V2ProviderAddButton(
362509 verticalAlignment = Alignment.CenterVertically,
363510 horizontalArrangement = Arrangement.Center,
364511 ) {
365-Icon(imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.size(13.dp))
512+Icon(imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.size(17.dp))
366513Spacer(modifier = Modifier.width(7.dp))
367-Text(text = "Add Provider", style = ClawTheme.type.label, maxLines = 1)
514+Text(text = "Open Gateway Setup", style = ClawTheme.type.label, maxLines = 1)
368515 }
369516 }
370517}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。