package cc.unitmesh.devins.ui.compose.document import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField 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.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import cc.unitmesh.devins.ui.compose.icons.AutoDevComposeIcons import cc.unitmesh.devins.ui.compose.theme.AutoDevColors import cc.unitmesh.config.ConfigManager import cc.unitmesh.devins.workspace.Workspace import cc.unitmesh.indexer.DomainDictGenerator import cc.unitmesh.indexer.DomainDictService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext sealed class DomainDictStatus { object Idle : DomainDictStatus() object Loading : DomainDictStatus() data class Scanning(val message: String = "Scanning...") : DomainDictStatus() data class Generating(val progress: String = "", val lineCount: Int = 0) : DomainDictStatus() data class Success(val message: String) : DomainDictStatus() data class Error(val message: String) : DomainDictStatus() } data class DictEntry( val chinese: String, val english: String, val description: String ) { fun getCodeTranslations(): List = english.split("|").map { it.trim() }.filter { it.isNotEmpty() } fun toCsvLine(): String = "$chinese,$english,$description" companion object { fun fromCsvLine(line: String): DictEntry? { val parts = line.split(",", limit = 3) return if (parts.size >= 2) { DictEntry(parts[0].trim(), parts[1].trim(), parts.getOrElse(2) { "" }.trim()) } else null } fun parseCSV(content: String): List = content.lines().drop(1).filter { it.isNotBlank() }.mapNotNull { fromCsvLine(it) } fun toCSV(entries: List): String { val header = "Chinese,Code Translation,Description" return (listOf(header) + entries.map { it.toCsvLine() }).joinToString("\n") } } } @Composable fun DomainDictDialog( workspace: Workspace, onDismiss: () -> Unit ) { var entries by remember { mutableStateOf>(emptyList()) } var status by remember { mutableStateOf(DomainDictStatus.Idle) } var hasChanges by remember { mutableStateOf(false) } var streamingContent by remember { mutableStateOf("") } val scope = rememberCoroutineScope() val domainDictService = remember { DomainDictService(workspace.fileSystem) } val listState = rememberLazyListState() LaunchedEffect(Unit) { status = DomainDictStatus.Loading try { val content = withContext(Dispatchers.Default) { domainDictService.loadContent() } entries = DictEntry.parseCSV(content ?: "") status = DomainDictStatus.Idle } catch (e: Exception) { status = DomainDictStatus.Error("Load failed: ${e.message}") } } fun saveDict() { scope.launch { status = DomainDictStatus.Loading try { val saved = withContext(Dispatchers.Default) { domainDictService.saveContent(DictEntry.toCSV(entries)) } status = if (saved) { hasChanges = false DomainDictStatus.Success("Saved ${entries.size} entries") } else DomainDictStatus.Error("Save failed") } catch (e: Exception) { status = DomainDictStatus.Error("Save failed: ${e.message}") } } } fun generateDict() { scope.launch { status = DomainDictStatus.Scanning() streamingContent = "" try { val config = ConfigManager.load().getActiveModelConfig() if (config == null || !config.isValid()) { status = DomainDictStatus.Error("No valid LLM config") return@launch } val generator = DomainDictGenerator(workspace.fileSystem, config) var lineCount = 0 val result = StringBuilder() generator.generateStreaming().collect { chunk -> result.append(chunk) streamingContent = result.toString() val newCount = streamingContent.count { it == '\n' } if (newCount > lineCount) { lineCount = newCount status = DomainDictStatus.Generating("", lineCount) } else if (status is DomainDictStatus.Scanning) { status = DomainDictStatus.Generating("", 0) } } val saved = withContext(Dispatchers.Default) { domainDictService.saveContent(result.toString()) } if (saved) { entries = DictEntry.parseCSV(result.toString()) hasChanges = false streamingContent = "" status = DomainDictStatus.Success("Generated ${entries.size} entries") } else { status = DomainDictStatus.Error("Save failed") } } catch (e: Exception) { status = DomainDictStatus.Error("Failed: ${e.message}") } } } val isLoading = status is DomainDictStatus.Loading val isGenerating = status is DomainDictStatus.Generating || status is DomainDictStatus.Scanning Dialog( onDismissRequest = { if (!isGenerating) onDismiss() }, properties = DialogProperties(usePlatformDefaultWidth = false) ) { Surface( modifier = Modifier.width(620.dp).heightIn(max = 600.dp), shape = RoundedCornerShape(10.dp), color = MaterialTheme.colorScheme.surface, tonalElevation = 2.dp ) { Column(modifier = Modifier.fillMaxWidth()) { // Compact header Row( modifier = Modifier.fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceContainerLow) .padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(AutoDevComposeIcons.MenuBook, null, Modifier.size(20.dp), AutoDevColors.Indigo.c500) Text("Domain Dictionary", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) if (hasChanges) Text("*", color = AutoDevColors.Amber.c500, fontWeight = FontWeight.Bold) Text("(${entries.size})", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) } IconButton(onClick = { if (!isGenerating) onDismiss() }, Modifier.size(28.dp)) { Icon(AutoDevComposeIcons.Close, "Close", Modifier.size(18.dp)) } } // Status bar (only when needed) when (val s = status) { is DomainDictStatus.Generating, is DomainDictStatus.Scanning -> { Row( Modifier.fillMaxWidth().background(AutoDevColors.Indigo.c100.copy(0.5f)).padding(8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp, color = AutoDevColors.Indigo.c500) Text( if (s is DomainDictStatus.Generating) "Generating... (${s.lineCount})" else "Scanning...", fontSize = 12.sp, color = AutoDevColors.Indigo.c700 ) } } is DomainDictStatus.Success -> { Row(Modifier.fillMaxWidth().background(AutoDevColors.Green.c100.copy(0.5f)).padding(8.dp)) { Icon(AutoDevComposeIcons.CheckCircle, null, Modifier.size(14.dp), AutoDevColors.Green.c600) Spacer(Modifier.width(6.dp)) Text(s.message, fontSize = 12.sp, color = AutoDevColors.Green.c700) } } is DomainDictStatus.Error -> { Row(Modifier.fillMaxWidth().background(AutoDevColors.Red.c100.copy(0.5f)).padding(8.dp)) { Icon(AutoDevComposeIcons.Error, null, Modifier.size(14.dp), AutoDevColors.Red.c600) Spacer(Modifier.width(6.dp)) Text(s.message, fontSize = 12.sp, color = AutoDevColors.Red.c700) } } else -> {} } // Content Box(Modifier.weight(1f).fillMaxWidth().padding(8.dp)) { when { isGenerating && streamingContent.isNotEmpty() -> StreamingView(streamingContent) isLoading -> LoadingView() else -> TableView(entries, listState, { i, e -> entries = entries.toMutableList().apply { set(i, e) } hasChanges = true }, { i -> entries = entries.toMutableList().apply { removeAt(i) } hasChanges = true }, !isLoading && !isGenerating) } } // Actions Row( Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceContainerLow).padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { OutlinedButton( onClick = { entries = entries + DictEntry("", "", "") hasChanges = true scope.launch { listState.animateScrollToItem(entries.size - 1) } }, enabled = !isLoading && !isGenerating, modifier = Modifier.height(32.dp), contentPadding = PaddingValues(horizontal = 10.dp) ) { Icon(AutoDevComposeIcons.Add, null, Modifier.size(14.dp)) Spacer(Modifier.width(4.dp)) Text("Add", fontSize = 12.sp) } Spacer(Modifier.weight(1f)) OutlinedButton( onClick = { generateDict() }, enabled = !isLoading && !isGenerating, modifier = Modifier.height(32.dp), contentPadding = PaddingValues(horizontal = 10.dp), colors = ButtonDefaults.outlinedButtonColors(contentColor = AutoDevColors.Indigo.c600), border = BorderStroke(1.dp, AutoDevColors.Indigo.c300) ) { Icon(AutoDevComposeIcons.Custom.AI, null, Modifier.size(14.dp)) Spacer(Modifier.width(4.dp)) Text(if (isGenerating) "..." else "Generate", fontSize = 12.sp) } Button( onClick = { saveDict() }, enabled = hasChanges && !isLoading && !isGenerating, modifier = Modifier.height(32.dp), contentPadding = PaddingValues(horizontal = 12.dp), colors = ButtonDefaults.buttonColors(AutoDevColors.Indigo.c600, Color.White) ) { Icon(AutoDevComposeIcons.Save, null, Modifier.size(14.dp)) Spacer(Modifier.width(4.dp)) Text("Save", fontSize = 12.sp) } } } } } } @Composable private fun LoadingView() { Box(Modifier.fillMaxSize(), Alignment.Center) { CircularProgressIndicator(Modifier.size(24.dp), color = AutoDevColors.Indigo.c500) } } @Composable private fun StreamingView(content: String) { val lines = remember(content) { content.lines() } val scrollState = rememberLazyListState() LaunchedEffect(lines.size) { if (lines.isNotEmpty()) scrollState.animateScrollToItem(lines.size - 1) } Surface(Modifier.fillMaxSize(), RoundedCornerShape(6.dp), border = BorderStroke(1.dp, AutoDevColors.Indigo.c200)) { LazyColumn(state = scrollState, modifier = Modifier.padding(8.dp)) { items(lines.size) { i -> Text(lines[i], fontSize = 11.sp, fontFamily = FontFamily.Monospace, lineHeight = 14.sp) } } } } @Composable private fun TableView( entries: List, listState: androidx.compose.foundation.lazy.LazyListState, onUpdate: (Int, DictEntry) -> Unit, onDelete: (Int) -> Unit, enabled: Boolean ) { Surface(Modifier.fillMaxSize(), RoundedCornerShape(6.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)) { Column { // Header Row( Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceContainerLow).padding(horizontal = 8.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically ) { Text("Term", Modifier.width(80.dp), fontSize = 11.sp, fontWeight = FontWeight.SemiBold) Text("Code Translations", Modifier.weight(1f), fontSize = 11.sp, fontWeight = FontWeight.SemiBold) Text("Description", Modifier.weight(0.6f), fontSize = 11.sp, fontWeight = FontWeight.SemiBold) Spacer(Modifier.width(24.dp)) } HorizontalDivider(thickness = 1.dp) if (entries.isEmpty()) { Box(Modifier.fillMaxSize(), Alignment.Center) { Text("No entries. Add or generate.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) } } else { LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { itemsIndexed(entries, key = { i, _ -> i }) { i, entry -> TableRow(entry, i, { onUpdate(i, it) }, { onDelete(i) }, enabled) if (i < entries.size - 1) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(0.5f)) } } } } } } @Composable private fun TableRow( entry: DictEntry, index: Int, onUpdate: (DictEntry) -> Unit, onDelete: () -> Unit, enabled: Boolean ) { var chinese by remember(entry) { mutableStateOf(entry.chinese) } var english by remember(entry) { mutableStateOf(entry.english) } var description by remember(entry) { mutableStateOf(entry.description) } var editingCode by remember { mutableStateOf(false) } val codes = remember(english) { english.split("|").map { it.trim() }.filter { it.isNotEmpty() } } Row( Modifier.fillMaxWidth() .background(if (index % 2 == 1) MaterialTheme.colorScheme.surfaceContainerLow.copy(0.3f) else Color.Transparent) .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { // Chinese term MiniField(chinese, { chinese = it; onUpdate(DictEntry(it, english, description)) }, "Term", enabled, Modifier.width(80.dp)) // Code translations - chips or edit Box(Modifier.weight(1f).padding(horizontal = 4.dp)) { if (editingCode) { MiniField( english, { english = it; onUpdate(DictEntry(chinese, it, description)) }, "Class | Method", enabled, Modifier.fillMaxWidth().onFocusChanged { if (!it.isFocused) editingCode = false }, FontFamily.Monospace ) } else { Row( Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()) .clickable(enabled) { editingCode = true } .padding(vertical = 2.dp), horizontalArrangement = Arrangement.spacedBy(3.dp), verticalAlignment = Alignment.CenterVertically ) { if (codes.isEmpty()) { Text("click to add...", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f)) } else { codes.forEach { code -> Surface( shape = RoundedCornerShape(3.dp), color = AutoDevColors.Cyan.c100.copy(0.6f), border = BorderStroke(0.5.dp, AutoDevColors.Cyan.c300.copy(0.4f)) ) { Text( code, Modifier.padding(horizontal = 4.dp, vertical = 1.dp), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = AutoDevColors.Cyan.c800, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } } } } } // Description MiniField(description, { description = it; onUpdate(DictEntry(chinese, english, it)) }, "Desc", enabled, Modifier.weight(0.6f)) // Delete IconButton(onClick = onDelete, enabled = enabled, modifier = Modifier.size(20.dp)) { Icon(AutoDevComposeIcons.Delete, "Delete", Modifier.size(12.dp), MaterialTheme.colorScheme.error.copy(0.5f)) } } } @Composable private fun MiniField( value: String, onValueChange: (String) -> Unit, placeholder: String, enabled: Boolean, modifier: Modifier = Modifier, fontFamily: FontFamily = FontFamily.Default ) { BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier .clip(RoundedCornerShape(3.dp)) .background(if (enabled) MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.3f) else Color.Transparent) .padding(horizontal = 6.dp, vertical = 4.dp), textStyle = TextStyle(fontSize = 11.sp, fontFamily = fontFamily, color = MaterialTheme.colorScheme.onSurface), cursorBrush = SolidColor(AutoDevColors.Indigo.c500), enabled = enabled, singleLine = true, decorationBox = { inner -> Box { if (value.isEmpty()) Text(placeholder, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f)) inner() } } ) }