package cc.unitmesh.agent.subagent /** * Regex pattern to match Image components in NanoDSL code. * Matches multiline Image(src="...", ...) or Image(src=..., ...) * Uses [\s\S] instead of . to match any character including newlines (JS compatible). */ val imagePattern = Regex( """Image\s*\([\s\S]*?src\s*=\s*["']([^"']+)["']""" ) /** * Check if a src value should be replaced with AI-generated image. * Since LLM often generates fake/hallucinated URLs, we replace all src values * except for data: URLs (which are actual embedded images). */ fun shouldGenerateImage(src: String): Boolean { val trimmed = src.trim() // Only skip data: URLs (actual embedded image data) // All other URLs (including http/https) should be replaced since they're usually fake return !trimmed.startsWith("data:") } /** * Check if a src value is a valid image that can be displayed directly. * Returns true only for data: URLs which are actual embedded images. * All http/https URLs generated by LLM are considered invalid. */ fun isValidImageSrc(src: String): Boolean { val trimmed = src.trim() // Only data: URLs are valid - these are actual base64 encoded images // All http/https URLs from LLM are fake and should not be loaded return trimmed.startsWith("data:image/", ignoreCase = true) } /** * Extract a prompt from surrounding NanoDSL context. * Looks for nearby Text components that might describe the image. */ fun extractContextPrompt(context: String): String { if (context.isEmpty()) return "" // Prefer the Text node that is closest to the Image node in the provided snippet. // `NanoDSLAgent` passes a substring around the Image match, so picking the closest // description usually yields the best prompt. val imageIndex = context.indexOf("Image(") // Look for Text components with meaningful content val textPattern = Regex("""Text\s*\(\s*["']([^"']+)["']""") val textMatches = textPattern.findAll(context).toList() fun isMeaningfulText(text: String): Boolean { if (text.length <= 3) return false return !text.matches( Regex( "^(Click|Submit|Cancel|OK|Yes|No|Close|Open|Edit|Delete|Save|Back|Next|Previous)$", RegexOption.IGNORE_CASE ) ) } val meaningfulMatches = textMatches .map { match -> match.range.first to match.groupValues[1] } .filter { (_, text) -> isMeaningfulText(text) } if (meaningfulMatches.isEmpty()) return "" // If we can locate an Image in this context snippet, pick the closest Text. // Prefer a Text that appears before the Image when distances tie. if (imageIndex >= 0) { val best = meaningfulMatches.minWithOrNull( compareBy> { (pos, _) -> kotlin.math.abs(pos - imageIndex) } .thenBy { (pos, _) -> if (pos <= imageIndex) 0 else 1 } ) return best?.second ?: "" } // Fallback: the first meaningful Text. return meaningfulMatches.first().second } private fun isLikelyNoisyOrTokenizedUrl(src: String): Boolean { val trimmed = src.trim() if (!(trimmed.startsWith("http://") || trimmed.startsWith("https://"))) return false // Common signing/token query params (case-insensitive). val lower = trimmed.lowercase() if (lower.contains("signature=") || lower.contains("expires=") || lower.contains("token=") || lower.contains("publickey=")) { return true } // Heuristic: too many digits indicates an auto-generated object key / timestamp / hash. // We bias toward considering it noisy when the string is long and digit-dense. val totalLen = trimmed.length if (totalLen < 40) return false val digitCount = trimmed.count { it.isDigit() } val letterCount = trimmed.count { it.isLetter() } val digitRatio = digitCount.toDouble() / totalLen.toDouble() // Examples we want to catch: // - object keys with timestamps + hashes // - signed URLs with long numeric expirations return (digitCount >= 16 && digitRatio >= 0.25) || (digitCount >= letterCount * 2 && digitCount >= 20) } private fun extractPromptFromSrc(src: String): String? { if (src.isBlank()) return null // data: URLs are already real embedded images; they usually don't contain useful prompts. val trimmed = src.trim() if (trimmed.startsWith("data:")) return null // Signed / tokenized URLs often contain lots of numbers and are not meaningful prompts. // In that case, fall back to extracting prompt from nearby Text blocks. if (isLikelyNoisyOrTokenizedUrl(trimmed)) return null // Handle URLs - try to extract meaningful parts val urlCleaned = if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { // For URLs, try to extract path segments that might be meaningful trimmed .replace(Regex("^https?://[^/]+/"), "") // Remove domain .replace(Regex("\\?.*$"), "") // Remove query string .replace(Regex("photo-[0-9a-f-]+"), "") // Remove Unsplash photo IDs .replace(Regex("[0-9]+x[0-9]+"), "") // Remove dimensions } else { trimmed } // Clean up the path/URL val cleaned = urlCleaned .replace(Regex("^[./]+"), "") .replace(Regex("\\.(jpg|jpeg|png|gif|webp|svg)$", RegexOption.IGNORE_CASE), "") .replace("/", " ") .replace("_", " ") .replace("-", " ") .replace(Regex("\\s+"), " ") .trim() // If it looks like a variable reference (e.g., item.image), extract the meaningful part val candidate = if (cleaned.contains(".")) { cleaned.split(".") .lastOrNull() ?.replace(Regex("[^a-zA-Z0-9 ]"), " ") ?.replace(Regex("\\s+"), " ") ?.trim() ?.takeIf { it.isNotEmpty() } ?: cleaned } else { cleaned } if (candidate.length > 3 && candidate.any { it.isLetter() }) return candidate return null } /** * Extract a meaningful prompt from the src value and surrounding context. * Works with URLs (including fake Unsplash links), paths, and placeholders. */ fun extractImagePrompt(src: String, surroundingContext: String = ""): String { // Prefer deriving prompt from src when it contains meaningful information. // This avoids picking unrelated nearby Text like titles (e.g., "Singapore Trip Planner"). val srcPrompt = extractPromptFromSrc(src) if (srcPrompt != null) return srcPrompt // Otherwise, try to extract meaningful text from the surrounding NanoDSL context. val contextPrompt = extractContextPrompt(surroundingContext) if (contextPrompt.isNotEmpty()) return contextPrompt // Fallback: return a generic prompt based on context or default return "high quality image" }