class AiModelInfo { const AiModelInfo({ required this.id, required this.kind, required this.name, required this.version, required this.size, required this.memoryRequired, required this.recommendedTier, required this.downloaded, required this.downloadProgress, }); final String id; final String kind; final String name; final String version; final int size; final int memoryRequired; final String recommendedTier; final bool downloaded; final int downloadProgress; String get kindLabel { switch (kind.toLowerCase()) { case 'llm': return 'LLM'; case 'asr': return 'ASR'; case 'tts': return 'TTS'; default: return kind.toUpperCase(); } } String get sizeLabel => _formatBytes(size); String get memoryLabel => _formatBytes(memoryRequired); factory AiModelInfo.fromJson(Map json) { return AiModelInfo( id: json['id']?.toString() ?? '', kind: json['kind']?.toString() ?? '', name: json['name']?.toString() ?? json['id']?.toString() ?? '', version: json['version']?.toString() ?? '', size: _asInt(json['size']), memoryRequired: _asInt(json['memory_required']), recommendedTier: json['recommended_tier']?.toString() ?? '', downloaded: json['downloaded'] == true, downloadProgress: _asInt(json['download_progress']), ); } static int _asInt(dynamic value) { if (value is int) return value; if (value is num) return value.toInt(); return int.tryParse(value?.toString() ?? '') ?? 0; } static String _formatBytes(int bytes) { if (bytes <= 0) return '0 B'; if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) { return '${(bytes / 1024).toStringAsFixed(1)} KB'; } if (bytes < 1024 * 1024 * 1024) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; } } class ModelsSnapshot { const ModelsSnapshot({ required this.models, required this.active, required this.usedSpace, required this.quota, }); final List models; final Map active; final int usedSpace; final int quota; String get usedLabel => AiModelInfo._formatBytes(usedSpace); String get quotaLabel => AiModelInfo._formatBytes(quota); double get usageRatio => quota <= 0 ? 0 : (usedSpace / quota).clamp(0.0, 1.0); bool isActive(AiModelInfo model) { final current = active[model.kind.toLowerCase()]; return current != null && current == model.id; } factory ModelsSnapshot.fromJson(Map json) { final rawModels = json['models']; final models = []; if (rawModels is List) { for (final item in rawModels) { if (item is Map) { models.add(AiModelInfo.fromJson(item)); } else if (item is Map) { models.add(AiModelInfo.fromJson(Map.from(item))); } } } final active = {}; final rawActive = json['active']; if (rawActive is Map) { rawActive.forEach((key, value) { active[key.toString().toLowerCase()] = value.toString(); }); } return ModelsSnapshot( models: models, active: active, usedSpace: AiModelInfo._asInt(json['used_space']), quota: AiModelInfo._asInt(json['quota']), ); } }