Skip to content

Commit 3d3be42

Browse files
committed
feat: Add artifact-specific UI components with compact code display and improved preview #526
- Add ArtifactCodeBlock for compact, collapsible artifact code display with stats and copy functionality - Add ArtifactMessageList for optimized artifact generation UI with auto-scroll and streaming support - Add ArtifactStreamingIndicator and ArtifactThinkingBlock for better visual feedback - Update ArtifactPreviewPanel to support .unit bundle format saving and add reload button - Replace AgentMessageList with ArtifactMessageList in ArtifactPage for cleaner artifact-focused UI - Improve preview panel with streaming overlay and reload functionality after generation completes
1 parent 991d379 commit 3d3be42

File tree

6 files changed

+1154
-128
lines changed

6 files changed

+1154
-128
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package cc.unitmesh.devins.ui.compose.agent.artifact
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.horizontalScroll
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.rememberScrollState
7+
import androidx.compose.foundation.shape.RoundedCornerShape
8+
import androidx.compose.foundation.text.selection.SelectionContainer
9+
import androidx.compose.foundation.verticalScroll
10+
import androidx.compose.material.icons.Icons
11+
import androidx.compose.material.icons.filled.*
12+
import androidx.compose.material3.*
13+
import androidx.compose.runtime.*
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.platform.LocalClipboardManager
17+
import androidx.compose.ui.text.AnnotatedString
18+
import androidx.compose.ui.text.font.FontFamily
19+
import androidx.compose.ui.text.font.FontWeight
20+
import androidx.compose.ui.text.style.TextOverflow
21+
import androidx.compose.ui.unit.Dp
22+
import androidx.compose.ui.unit.dp
23+
import androidx.compose.ui.unit.sp
24+
import cc.unitmesh.agent.Platform
25+
import cc.unitmesh.devins.ui.compose.icons.AutoDevComposeIcons
26+
27+
/**
28+
* Compact code block display for artifacts.
29+
* Collapsed by default - shows only header with title and stats.
30+
* Users can expand to see the full code.
31+
*
32+
* Similar to ToolCallItem pattern but optimized for showing generated artifact code.
33+
*/
34+
@Composable
35+
fun ArtifactCodeBlock(
36+
title: String,
37+
code: String,
38+
language: String = "html",
39+
isStreaming: Boolean = false,
40+
identifier: String? = null,
41+
modifier: Modifier = Modifier
42+
) {
43+
// Key the expanded state by title/language so it never "leaks" across different files/runs.
44+
var expanded by remember(identifier ?: title, language) { mutableStateOf(false) }
45+
val clipboardManager = LocalClipboardManager.current
46+
val isDesktop = Platform.isJvm && !Platform.isAndroid
47+
48+
// Ensure a new streaming run always starts collapsed (even if identifier/title repeats).
49+
LaunchedEffect(isStreaming, identifier, title) {
50+
if (isStreaming) {
51+
expanded = false
52+
}
53+
}
54+
55+
// Stats for display
56+
val lineCount = code.lines().size
57+
val charCount = code.length
58+
val displayStats = buildString {
59+
append("$lineCount lines")
60+
if (charCount > 1000) {
61+
append(" · ${charCount / 1000}k chars")
62+
} else {
63+
append(" · $charCount chars")
64+
}
65+
}
66+
67+
Surface(
68+
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
69+
shape = RoundedCornerShape(8.dp),
70+
modifier = modifier
71+
) {
72+
Column(modifier = Modifier.padding(0.dp)) {
73+
// Header - always visible. Expansion is explicit via the arrow button (no "click anywhere" toggle).
74+
Row(
75+
modifier = Modifier
76+
.fillMaxWidth()
77+
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
78+
.padding(horizontal = 12.dp, vertical = 10.dp),
79+
verticalAlignment = Alignment.CenterVertically,
80+
horizontalArrangement = Arrangement.SpaceBetween
81+
) {
82+
Row(
83+
verticalAlignment = Alignment.CenterVertically,
84+
horizontalArrangement = Arrangement.spacedBy(8.dp),
85+
modifier = Modifier.weight(1f)
86+
) {
87+
// Status icon
88+
if (isStreaming) {
89+
CircularProgressIndicator(
90+
modifier = Modifier.size(14.dp),
91+
strokeWidth = 2.dp,
92+
color = MaterialTheme.colorScheme.primary
93+
)
94+
} else {
95+
Icon(
96+
imageVector = AutoDevComposeIcons.Code,
97+
contentDescription = null,
98+
modifier = Modifier.size(16.dp),
99+
tint = MaterialTheme.colorScheme.primary
100+
)
101+
}
102+
103+
// Title
104+
Text(
105+
text = title,
106+
style = MaterialTheme.typography.labelMedium,
107+
fontWeight = FontWeight.Medium,
108+
color = MaterialTheme.colorScheme.onSurface,
109+
maxLines = 1,
110+
overflow = TextOverflow.Ellipsis
111+
)
112+
113+
// Language badge
114+
Surface(
115+
shape = RoundedCornerShape(4.dp),
116+
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
117+
) {
118+
Text(
119+
text = language.uppercase(),
120+
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp),
121+
color = MaterialTheme.colorScheme.primary,
122+
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
123+
)
124+
}
125+
}
126+
127+
Row(
128+
verticalAlignment = Alignment.CenterVertically,
129+
horizontalArrangement = Arrangement.spacedBy(4.dp)
130+
) {
131+
// Stats
132+
Text(
133+
text = displayStats,
134+
style = MaterialTheme.typography.labelSmall,
135+
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
136+
)
137+
138+
// Copy button
139+
IconButton(
140+
onClick = { clipboardManager.setText(AnnotatedString(code)) },
141+
modifier = Modifier.size(28.dp)
142+
) {
143+
Icon(
144+
imageVector = AutoDevComposeIcons.ContentCopy,
145+
contentDescription = "Copy code",
146+
modifier = Modifier.size(16.dp),
147+
tint = MaterialTheme.colorScheme.onSurfaceVariant
148+
)
149+
}
150+
151+
// Expand/collapse button (explicit)
152+
IconButton(
153+
onClick = { expanded = !expanded },
154+
modifier = Modifier.size(28.dp)
155+
) {
156+
Icon(
157+
imageVector = if (expanded) AutoDevComposeIcons.ExpandLess else AutoDevComposeIcons.ExpandMore,
158+
contentDescription = if (expanded) "Collapse" else "Expand",
159+
modifier = Modifier.size(20.dp),
160+
tint = MaterialTheme.colorScheme.onSurfaceVariant
161+
)
162+
}
163+
}
164+
}
165+
166+
// Code content - only shown when expanded
167+
if (expanded) {
168+
Box(
169+
modifier = Modifier.fillMaxWidth()
170+
) {
171+
val horizontalScrollState = rememberScrollState()
172+
173+
SelectionContainer {
174+
Text(
175+
text = code,
176+
style = MaterialTheme.typography.bodySmall.copy(
177+
fontFamily = FontFamily.Monospace,
178+
fontSize = if (isDesktop) 12.sp else 11.sp,
179+
lineHeight = if (isDesktop) 18.sp else 16.sp
180+
),
181+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f),
182+
modifier = Modifier
183+
.fillMaxWidth()
184+
.horizontalScroll(horizontalScrollState)
185+
.padding(12.dp)
186+
)
187+
}
188+
}
189+
}
190+
}
191+
}
192+
}
193+
194+
/**
195+
* Streaming indicator for artifact generation
196+
*/
197+
@Composable
198+
fun ArtifactStreamingIndicator(
199+
title: String,
200+
charCount: Int,
201+
modifier: Modifier = Modifier
202+
) {
203+
Surface(
204+
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f),
205+
shape = RoundedCornerShape(8.dp),
206+
modifier = modifier
207+
) {
208+
Row(
209+
modifier = Modifier
210+
.fillMaxWidth()
211+
.padding(12.dp),
212+
verticalAlignment = Alignment.CenterVertically,
213+
horizontalArrangement = Arrangement.spacedBy(12.dp)
214+
) {
215+
CircularProgressIndicator(
216+
modifier = Modifier.size(20.dp),
217+
strokeWidth = 2.dp,
218+
color = MaterialTheme.colorScheme.primary
219+
)
220+
221+
Column(modifier = Modifier.weight(1f)) {
222+
Text(
223+
text = "Generating: $title",
224+
style = MaterialTheme.typography.labelMedium,
225+
fontWeight = FontWeight.Medium,
226+
color = MaterialTheme.colorScheme.onSurface
227+
)
228+
Text(
229+
text = "$charCount characters generated...",
230+
style = MaterialTheme.typography.labelSmall,
231+
color = MaterialTheme.colorScheme.onSurfaceVariant
232+
)
233+
}
234+
}
235+
}
236+
}
237+
238+
/**
239+
* Thinking indicator display (like Claude's thinking blocks)
240+
*/
241+
@Composable
242+
fun ArtifactThinkingBlock(
243+
content: String,
244+
modifier: Modifier = Modifier
245+
) {
246+
if (content.isBlank()) return
247+
248+
Surface(
249+
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f),
250+
shape = RoundedCornerShape(6.dp),
251+
modifier = modifier
252+
) {
253+
Row(
254+
modifier = Modifier.padding(8.dp),
255+
horizontalArrangement = Arrangement.spacedBy(8.dp),
256+
verticalAlignment = Alignment.Top
257+
) {
258+
Icon(
259+
imageVector = Icons.Default.Psychology,
260+
contentDescription = "Thinking",
261+
modifier = Modifier.size(16.dp),
262+
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
263+
)
264+
265+
val scrollState = rememberScrollState()
266+
Text(
267+
text = content,
268+
style = MaterialTheme.typography.bodySmall.copy(
269+
fontFamily = FontFamily.Monospace,
270+
fontSize = 11.sp,
271+
lineHeight = 14.sp
272+
),
273+
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
274+
modifier = Modifier
275+
.weight(1f)
276+
.heightIn(max = 60.dp)
277+
.verticalScroll(scrollState)
278+
)
279+
}
280+
}
281+
}
282+

0 commit comments

Comments
 (0)