1→import Foundation 2→import SwiftUI 3→ 4→// MARK: - AI Types 5→// Consolidated AI-related types from AIAnalysisResult.swift 6→// Contains: AIAnalysisResult, SentimentType, ArticleCategory, ExtractedEntities, 7→// EntityType, AggregatedInsights, TrendAnalysis, LMStudioResponse, AnalysisRequest 8→ 9→// MARK: - AI Analysis Result 10→ 11→/// Result of AI analysis for a single article 12→struct AIAnalysisResult: Codable { 13→ let summary: String 14→ let category: ArticleCategory 15→ let sentiment: SentimentType 16→ let sentimentScore: Double 17→ let relevanceScore: Double 18→ let keyTopics: [String] 19→ var entities: ExtractedEntities? // NER results 20→ 21→ enum SentimentType: String, Codable, CaseIterable { 22→ case positive = "positivo" 23→ case negative = "negativo" 24→ case neutral = "neutro" 25→ 26→ var displayName: String { 27→ switch self { 28→ case .positive: return "Positivo" 29→ case .negative: return "Negativo" 30→ case .neutral: return "Neutro" 31→ } 32→ } 33→ 34→ var emoji: String { 35→ switch self { 36→ case .positive: return "+" 37→ case .negative: return "-" 38→ case .neutral: return "~" 39→ } 40→ } 41→ } 42→ 43→ enum ArticleCategory: String, Codable, CaseIterable { 44→ case market = "Mercado" 45→ case regulation = "Regulação" 46→ case opinion = "Opinião" 47→ case politics = "Política" 48→ case corporate = "Corporativo" 49→ case international = "Internacional" 50→ case technology = "Tecnologia" 51→ case economy = "Economia" 52→ case unknown = "Outros" 53→ 54→ static func from(_ string: String) -> ArticleCategory { 55→ let normalized = string.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) 56→ return ArticleCategory.allCases.first { 57→ $0.rawValue.lowercased() == normalized 58→ } ?? .unknown 59→ } 60→ } 61→} 62→ 63→// MARK: - Extracted Entities (NER) 64→ 65→/// Entities extracted from article text using Named Entity Recognition 66→struct ExtractedEntities: Codable { 67→ let people: [String] // Person names 68→ let organizations: [String] // Company/organization names 69→ let locations: [String] // Geographic locations 70→ let monetaryValues: [String] // Money/currency values 71→ 72→ static var empty: ExtractedEntities { 73→ ExtractedEntities(people: [], organizations: [], locations: [], monetaryValues: []) 74→ } 75→ 76→ var isEmpty: Bool { 77→ people.isEmpty && organizations.isEmpty && locations.isEmpty && monetaryValues.isEmpty 78→ } 79→ 80→ var totalCount: Int { 81→ people.count + organizations.count + locations.count + monetaryValues.count 82→ } 83→} 84→ 85→// MARK: - Entity Type 86→ 87→/// Types of entities for filtering and display 88→enum EntityType: String, CaseIterable, Identifiable { 89→ case people = "Pessoas" 90→ case organizations = "Organizações" 91→ case locations = "Locais" 92→ case monetaryValues = "Valores" 93→ 94→ var id: String { rawValue } 95→ 96→ var icon: String { 97→ switch self { 98→ case .people: return "person.fill" 99→ case .organizations: return "building.2.fill" 100→ case .locations: return "mappin.circle.fill" 101→ case .monetaryValues: return "dollarsign.circle.fill" 102→ } 103→ } 104→ 105→ var color: Color { 106→ switch self { 107→ case .people: return .blue 108→ case .organizations: return .purple 109→ case .locations: return .green 110→ case .monetaryValues: return .orange 111→ } 112→ } 113→} 114→ 115→// MARK: - Aggregated Insights 116→ 117→/// Aggregated insights from multiple articles 118→struct AggregatedInsights: Codable { 119→ let executiveSummary: String 120→ let mainThemes: [String] 121→ let sentimentTrend: SentimentTrend 122→ let topSources: [SourceRelevance] 123→ let alerts: [AlertItem] 124→ let projection: String? 125→ let generatedAt: Date 126→ let articleCount: Int 127→ 128→ struct SentimentTrend: Codable { 129→ let overall: AIAnalysisResult.SentimentType 130→ let averageScore: Double 131→ let positiveCount: Int 132→ let negativeCount: Int 133→ let neutralCount: Int 134→ let dailyTrend: [DailySentiment] 135→ 136→ struct DailySentiment: Codable { 137→ let date: Date 138→ let averageScore: Double 139→ let count: Int 140→ } 141→ } 142→ 143→ struct SourceRelevance: Codable { 144→ let source: String 145→ let articleCount: Int 146→ let averageSentiment: Double 147→ let relevanceScore: Double 148→ } 149→ 150→ struct AlertItem: Codable, Identifiable { 151→ var id: String { articleId } 152→ let articleId: String 153→ let title: String 154→ let reason: AlertReason 155→ let severity: AlertSeverity 156→ 157→ enum AlertReason: String, Codable { 158→ case highRelevance = "Alta relevância" 159→ case extremeSentiment = "Sentimento extremo" 160→ case breakingNews = "Notícia urgente" 161→ case trendChange = "Mudança de tendência" 162→ } 163→ 164→ enum AlertSeverity: String, Codable { 165→ case high = "alta" 166→ case medium = "média" 167→ case low = "baixa" 168→ } 169→ } 170→} 171→ 172→// MARK: - Trend Analysis 173→ 174→/// Analysis of trends over time 175→struct TrendAnalysis: Codable { 176→ let period: TrendPeriod 177→ let sentimentChange: Double // Difference from previous period 178→ let volumeChange: Double // Article count difference 179→ let emergingTopics: [String] 180→ let decliningTopics: [String] 181→ let categoryDistribution: [CategoryCount] 182→ let comparison: PeriodComparison 183→ 184→ enum TrendPeriod: String, Codable { 185→ case daily = "diário" 186→ case weekly = "semanal" 187→ case monthly = "mensal" 188→ } 189→ 190→ struct CategoryCount: Codable { 191→ let category: AIAnalysisResult.ArticleCategory 192→ let count: Int 193→ let percentage: Double 194→ } 195→ 196→ struct PeriodComparison: Codable { 197→ let currentPeriodArticles: Int 198→ let previousPeriodArticles: Int 199→ let currentSentimentAvg: Double 200→ let previousSentimentAvg: Double 201→ } 202→} 203→ 204→// MARK: - LM Studio Response 205→ 206→/// Response structure from LM Studio API 207→struct LMStudioResponse: Codable { 208→ let id: String 209→ let object: String 210→ let created: Int 211→ let model: String 212→ let choices: [Choice] 213→ let usage: Usage? 214→ 215→ struct Choice: Codable { 216→ let index: Int 217→ let message: Message 218→ let finishReason: String? 219→ 220→ enum CodingKeys: String, CodingKey { 221→ case index 222→ case message 223→ case finishReason = "finish_reason" 224→ } 225→ } 226→ 227→ struct Message: Codable { 228→ let role: String 229→ let content: String 230→ } 231→ 232→ struct Usage: Codable { 233→ let promptTokens: Int 234→ let completionTokens: Int 235→ let totalTokens: Int 236→ 237→ enum CodingKeys: String, CodingKey { 238→ case promptTokens = "prompt_tokens" 239→ case completionTokens = "completion_tokens" 240→ case totalTokens = "total_tokens" 241→ } 242→ } 243→} 244→ 245→/// Response for model listing 246→struct LMStudioModelsResponse: Codable { 247→ let object: String 248→ let data: [ModelInfo] 249→ 250→ struct ModelInfo: Codable { 251→ let id: String 252→ let object: String 253→ let created: Int? 254→ let ownedBy: String? 255→ 256→ enum CodingKeys: String, CodingKey { 257→ case id 258→ case object 259→ case created 260→ case ownedBy = "owned_by" 261→ } 262→ } 263→} 264→ 265→// MARK: - Analysis Request 266→ 267→/// Request structure for AI analysis 268→struct AnalysisRequest: Codable { 269→ let model: String 270→ let messages: [ChatMessage] 271→ let temperature: Double 272→ let maxTokens: Int 273→ let stream: Bool 274→ 275→ struct ChatMessage: Codable { 276→ let role: String 277→ let content: String 278→ } 279→ 280→ enum CodingKeys: String, CodingKey { 281→ case model 282→ case messages 283→ case temperature 284→ case maxTokens = "max_tokens" 285→ case stream 286→ } 287→ 288→ static func forArticleAnalysis( 289→ title: String, 290→ summary: String, 291→ source: String, 292→ keyword: String, 293→ model: String 294→ ) -> AnalysisRequest { 295→ let prompt = """ 296→ Analise este artigo de notícia e retorne um JSON com: 297→ - summary: resumo em 2-3 frases (máx 150 palavras) 298→ - category: uma de [Mercado, Regulação, Opinião, Política, Corporativo, Internacional, Tecnologia, Economia] 299→ - sentiment: positivo, negativo ou neutro 300→ - sentimentScore: número de -1.0 (muito negativo) a 1.0 (muito positivo) 301→ - relevanceScore: 0.0 a 1.0 (relevância para a keyword "\(keyword)") 302→ - keyTopics: array de 3-5 tópicos principais 303→ - entities: objeto com extração de entidades: 304→ - people: array de nomes de pessoas mencionadas 305→ - organizations: array de empresas/instituições 306→ - locations: array de locais/cidades/países 307→ - monetaryValues: array de valores monetários (ex: "R$ 1 milhão", "US$ 50 bilhões") 308→ 309→ Título: \(title) 310→ Conteúdo: \(summary) 311→ Fonte: \(source) 312→ 313→ Responda APENAS com JSON válido, sem markdown ou explicações. 314→ """ 315→ 316→ return AnalysisRequest( 317→ model: model, 318→ messages: [ 319→ ChatMessage(role: "system", content: "Você é um analista de notícias especializado com habilidade em NER (Named Entity Recognition). Sempre responda em JSON válido."), 320→ ChatMessage(role: "user", content: prompt) 321→ ], 322→ temperature: 0.3, 323→ maxTokens: 700, 324→ stream: false 325→ ) 326→ } 327→ 328→ static func forAggregatedInsights( 329→ articles: [(title: String, summary: String, source: String, sentiment: String?)], 330→ keyword: String, 331→ model: String 332→ ) -> AnalysisRequest { 333→ let articlesJson = articles.prefix(50).enumerated().map { index, article in 334→ """ 335→ {"id": \(index + 1), "titulo": "\(article.title.prefix(100))", "fonte": "\(article.source)", "sentimento": "\(article.sentiment ?? "não analisado")"} 336→ """ 337→ }.joined(separator: ",\n") 338→ 339→ let prompt = """ 340→ Analise estes \(articles.count) artigos sobre "\(keyword)" e gere um relatório executivo em JSON: 341→ { 342→ "executiveSummary": "resumo geral em 3-5 frases", 343→ "mainThemes": ["tema1", "tema2", "tema3"], 344→ "overallSentiment": "positivo/negativo/neutro", 345→ "alerts": [{"title": "título do alerta", "reason": "motivo", "severity": "alta/média/baixa"}], 346→ "projection": "projeção baseada nas tendências" 347→ } 348→ 349→ Artigos: 350→ [\(articlesJson)] 351→ 352→ Responda APENAS com JSON válido. 353→ """ 354→ 355→ return AnalysisRequest( 356→ model: model, 357→ messages: [ 358→ ChatMessage(role: "system", content: "Você é um analista de mídia especializado em gerar relatórios executivos. Sempre responda em JSON válido."), 359→ ChatMessage(role: "user", content: prompt) 360→ ], 361→ temperature: 0.4, 362→ maxTokens: 1000, 363→ stream: false 364→ ) 365→ } 366→} 367→import Foundation 368→import SwiftUI 369→ 370→// MARK: - Filter Types 371→// Consolidated filter-related types from DatePeriod.swift and FilterConfig.swift 372→// Contains: DatePeriod, FilterConfig, AIFilterType, SortOptionType, 373→// EntitySelection, EntityCollection, EntityStats, FilterResult 374→ 375→// MARK: - Date Period Filter 376→ 377→/// Shared date period filter used across all views 378→/// Supports filtering articles by time periods 379→enum DatePeriod: String, CaseIterable, Identifiable { 380→ case all = "Todas" 381→ case today = "Hoje" 382→ case yesterday = "Ontem" 383→ case thisWeek = "Esta Semana" 384→ case thisMonth = "Este Mês" 385→ case last7Days = "7 dias" 386→ case last14Days = "14 dias" 387→ case last30Days = "30 dias" 388→ case last90Days = "90 dias" 389→ 390→ var id: String { rawValue } 391→ 392→ var icon: String { 393→ switch self { 394→ case .all: return "calendar" 395→ case .today: return "sun.max.fill" 396→ case .yesterday: return "sun.haze.fill" 397→ case .thisWeek: return "calendar.badge.clock" 398→ case .thisMonth: return "calendar.badge.plus" 399→ case .last7Days: return "7.circle.fill" 400→ case .last14Days: return "14.circle.fill" 401→ case .last30Days: return "30.circle.fill" 402→ case .last90Days: return "90.circle.fill" 403→ } 404→ } 405→ 406→ /// Short label for compact picker 407→ var shortLabel: String { 408→ switch self { 409→ case .all: return "Tudo" 410→ case .today: return "Hoje" 411→ case .yesterday: return "Ontem" 412→ case .thisWeek: return "Semana" 413→ case .thisMonth: return "Mês" 414→ case .last7Days: return "7d" 415→ case .last14Days: return "14d" 416→ case .last30Days: return "30d" 417→ case .last90Days: return "90d" 418→ } 419→ } 420→ 421→ /// Returns start date for this filter, or nil for "all" 422→ func startDate() -> Date? { 423→ let calendar = Calendar.current 424→ let now = Date() 425→ 426→ switch self { 427→ case .all: 428→ return nil 429→ case .today: 430→ return calendar.startOfDay(for: now) 431→ case .yesterday: 432→ guard let yesterday = calendar.date(byAdding: .day, value: -1, to: now) else { return nil } 433→ return calendar.startOfDay(for: yesterday) 434→ case .thisWeek: 435→ return calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: now)) 436→ case .thisMonth: 437→ return calendar.date(from: calendar.dateComponents([.year, .month], from: now)) 438→ case .last7Days: 439→ return calendar.date(byAdding: .day, value: -7, to: now) 440→ case .last14Days: 441→ return calendar.date(byAdding: .day, value: -14, to: now) 442→ case .last30Days: 443→ return calendar.date(byAdding: .day, value: -30, to: now) 444→ case .last90Days: 445→ return calendar.date(byAdding: .day, value: -90, to: now) 446→ } 447→ } 448→ 449→ /// Returns end date for this filter (only relevant for yesterday) 450→ func endDate() -> Date? { 451→ let calendar = Calendar.current 452→ let now = Date() 453→ 454→ switch self { 455→ case .yesterday: 456→ return calendar.startOfDay(for: now) 457→ default: 458→ return nil 459→ } 460→ } 461→ 462→ /// Returns date range tuple for filtering 463→ var dateRange: (start: Date, end: Date)? { 464→ guard let start = startDate() else { return nil } 465→ let end = endDate() ?? Date() 466→ return (start, end) 467→ } 468→ 469→ /// Number of days this period covers (for chart axis) 470→ var dayCount: Int { 471→ switch self { 472→ case .all: return 0 473→ case .today: return 1 474→ case .yesterday: return 1 475→ case .thisWeek: return 7 476→ case .thisMonth: return 30 477→ case .last7Days: return 7 478→ case .last14Days: return 14 479→ case .last30Days: return 30 480→ case .last90Days: return 90 481→ } 482→ } 483→ 484→ /// Compact periods for segmented picker (excludes detailed options) 485→ static var compactCases: [DatePeriod] { 486→ [.last7Days, .last14Days, .last30Days, .last90Days, .all] 487→ } 488→ 489→ /// Full list without the new extended periods (for backward compatibility) 490→ static var legacyCases: [DatePeriod] { 491→ [.all, .today, .yesterday, .thisWeek, .thisMonth, .last7Days, .last30Days] 492→ } 493→} 494→ 495→// MARK: - Article Filtering Extension 496→ 497→extension Array where Element == NewsArticle { 498→ /// Filter articles by date period 499→ func filtered(by period: DatePeriod) -> [NewsArticle] { 500→ guard let range = period.dateRange else { return self } 501→ return self.filter { article in 502→ article.publishedAt >= range.start && article.publishedAt <= range.end 503→ } 504→ } 505→} 506→ 507→// MARK: - Filter Configuration 508→ 509→/// Unified filter configuration for articles 510→/// Hashable for cache invalidation 511→struct FilterConfig: Hashable, Sendable { 512→ let keyword: String? 513→ let searchText: String 514→ let aiFilter: AIFilterType 515→ let entityFilter: EntitySelection? 516→ let showOnlyFavorites: Bool 517→ let sortOption: SortOptionType 518→ let sortAscending: Bool 519→ 520→ init( 521→ keyword: String? = nil, 522→ searchText: String = "", 523→ aiFilter: AIFilterType = .all, 524→ entityFilter: EntitySelection? = nil, 525→ showOnlyFavorites: Bool = false, 526→ sortOption: SortOptionType = .dateDesc, 527→ sortAscending: Bool = false 528→ ) { 529→ self.keyword = keyword 530→ self.searchText = searchText 531→ self.aiFilter = aiFilter 532→ self.entityFilter = entityFilter 533→ self.showOnlyFavorites = showOnlyFavorites 534→ self.sortOption = sortOption 535→ self.sortAscending = sortAscending 536→ } 537→ 538→ /// Cache key based on all filter parameters 539→ var cacheKey: Int { 540→ var hasher = Hasher() 541→ hasher.combine(keyword) 542→ hasher.combine(searchText.lowercased()) 543→ hasher.combine(aiFilter) 544→ hasher.combine(entityFilter) 545→ hasher.combine(showOnlyFavorites) 546→ hasher.combine(sortOption) 547→ hasher.combine(sortAscending) 548→ return hasher.finalize() 549→ } 550→} 551→ 552→// MARK: - AI Filter Type 553→ 554→enum AIFilterType: String, CaseIterable, Hashable, Sendable { 555→ case all = "Todos" 556→ case positive = "Positivos" 557→ case negative = "Negativos" 558→ case neutral = "Neutros" 559→ case highRelevance = "Alta Relevância" 560→ case critical = "Críticos" 561→ case unanalyzed = "Não Analisados" 562→ case analyzed = "Analisados" 563→ 564→ var icon: String { 565→ switch self { 566→ case .all: return "square.grid.2x2" 567→ case .positive: return "arrow.up.circle.fill" 568→ case .negative: return "arrow.down.circle.fill" 569→ case .neutral: return "minus.circle.fill" 570→ case .highRelevance: return "star.fill" 571→ case .critical: return "exclamationmark.triangle.fill" 572→ case .unanalyzed: return "brain.head.profile" 573→ case .analyzed: return "brain.filled.head.profile" 574→ } 575→ } 576→ 577→ var color: Color { 578→ switch self { 579→ case .all: return .blue 580→ case .positive: return .green 581→ case .negative: return .red 582→ case .neutral: return .gray 583→ case .highRelevance: return .yellow 584→ case .critical: return .orange 585→ case .unanalyzed: return .purple.opacity(0.5) 586→ case .analyzed: return .purple 587→ } 588→ } 589→} 590→ 591→// MARK: - Sort Option Type 592→ 593→enum SortOptionType: String, CaseIterable, Hashable, Sendable { 594→ case dateDesc = "Mais recentes" 595→ case dateAsc = "Mais antigos" 596→ case source = "Por fonte" 597→ case unreadFirst = "Não lidas primeiro" 598→ case byRelevance = "Por relevância IA" 599→ case bySentiment = "Por sentimento" 600→ case byPriority = "Por prioridade IA" 601→} 602→ 603→// MARK: - Entity Selection 604→ 605→struct EntitySelection: Hashable, Sendable { 606→ let entity: String 607→ let type: EntityTypeSelection 608→ 609→ enum EntityTypeSelection: String, Hashable, Sendable { 610→ case people 611→ case organizations 612→ case locations 613→ case monetaryValues 614→ } 615→} 616→ 617→// MARK: - Entity Collection 618→ 619→/// Cached decoded entities from a NewsArticle 620→struct EntityCollection: Hashable, Sendable { 621→ let people: [String] 622→ let organizations: [String] 623→ let locations: [String] 624→ let monetaryValues: [String] 625→ 626→ static let empty = EntityCollection( 627→ people: [], 628→ organizations: [], 629→ locations: [], 630→ monetaryValues: [] 631→ ) 632→ 633→ var isEmpty: Bool { 634→ people.isEmpty && organizations.isEmpty && locations.isEmpty && monetaryValues.isEmpty 635→ } 636→ 637→ var totalCount: Int { 638→ people.count + organizations.count + locations.count + monetaryValues.count 639→ } 640→} 641→ 642→// MARK: - Entity Statistics 643→ 644→struct EntityStats: Sendable { 645→ let people: [(name: String, count: Int)] 646→ let organizations: [(name: String, count: Int)] 647→ let locations: [(name: String, count: Int)] 648→ let monetaryValues: [(name: String, count: Int)] 649→ 650→ var isEmpty: Bool { 651→ people.isEmpty && organizations.isEmpty && locations.isEmpty && monetaryValues.isEmpty 652→ } 653→ 654→ var totalUniqueEntities: Int { 655→ people.count + organizations.count + locations.count + monetaryValues.count 656→ } 657→ 658→ var totalMentions: Int { 659→ people.reduce(0) { $0 + $1.count } + 660→ organizations.reduce(0) { $0 + $1.count } + 661→ locations.reduce(0) { $0 + $1.count } + 662→ monetaryValues.reduce(0) { $0 + $1.count } 663→ } 664→ 665→ static let empty = EntityStats( 666→ people: [], 667→ organizations: [], 668→ locations: [], 669→ monetaryValues: [] 670→ ) 671→} 672→ 673→// MARK: - Filter Result 674→ 675→struct FilterResult: Sendable { 676→ let articles: [String] // Article IDs 677→ let cacheKey: Int 678→ let timestamp: Date 679→ 680→ var isExpired: Bool { 681→ Date().timeIntervalSince(timestamp) > 60 // 1 minute cache 682→ } 683→} 684→import Foundation 685→import SwiftUI 686→ 687→// MARK: - Analysis Types 688→// Consolidated analysis-related types from AnalysisLogEntry.swift and TimelineTypes.swift 689→// Contains: LogLevel, AnalysisLogEntry, AnalysisStage, StageStatus, AnalysisPerformanceMetrics, 690→// ArticleAnalysisStatus, TimelineDataPoint, SentimentAnnotation, TimelinePeriod, 691→// TimelineSummary, AnalysisCheckpoint, CachedAnalysisStats, AnalysisPhase, AnalysisProgress 692→ 693→// MARK: - Log Level 694→ 695→/// Represents the severity level of an analysis log entry 696→enum LogLevel: String, Codable { 697→ case info 698→ case success 699→ case warning 700→ case error 701→ 702→ var icon: String { 703→ switch self { 704→ case .info: return "info.circle" 705→ case .success: return "checkmark.circle.fill" 706→ case .warning: return "exclamationmark.triangle.fill" 707→ case .error: return "xmark.circle.fill" 708→ } 709→ } 710→ 711→ var color: Color { 712→ switch self { 713→ case .info: return .secondary 714→ case .success: return .green 715→ case .warning: return .orange 716→ case .error: return .red 717→ } 718→ } 719→} 720→ 721→// MARK: - Analysis Log Entry 722→ 723→/// A single log entry during AI analysis 724→struct AnalysisLogEntry: Identifiable, Equatable { 725→ let id = UUID() 726→ let timestamp: Date 727→ let level: LogLevel 728→ let message: String 729→ let articleTitle: String? 730→ 731→ init(timestamp: Date = Date(), level: LogLevel = .info, message: String, articleTitle: String? = nil) { 732→ self.timestamp = timestamp 733→ self.level = level 734→ self.message = message 735→ self.articleTitle = articleTitle 736→ } 737→ 738→ var formattedTime: String { 739→ let formatter = DateFormatter() 740→ formatter.dateFormat = "HH:mm:ss" 741→ return formatter.string(from: timestamp) 742→ } 743→ 744→ static func == (lhs: AnalysisLogEntry, rhs: AnalysisLogEntry) -> Bool { 745→ lhs.id == rhs.id 746→ } 747→} 748→ 749→// MARK: - Analysis Stage 750→ 751→/// Represents a stage in the analysis pipeline 752→struct AnalysisStage: Identifiable, Equatable { 753→ let id: String 754→ let name: String 755→ var status: StageStatus 756→ var duration: TimeInterval? 757→ var startTime: Date? 758→ 759→ var formattedDuration: String? { 760→ guard let duration = duration else { return nil } 761→ return String(format: "%.1fs", duration) 762→ } 763→ 764→ mutating func start() { 765→ status = .inProgress 766→ startTime = Date() 767→ } 768→ 769→ mutating func complete() { 770→ status = .completed 771→ if let start = startTime { 772→ duration = Date().timeIntervalSince(start) 773→ } 774→ } 775→ 776→ mutating func fail() { 777→ status = .failed 778→ if let start = startTime { 779→ duration = Date().timeIntervalSince(start) 780→ } 781→ } 782→ 783→ mutating func skip() { 784→ status = .skipped 785→ } 786→} 787→ 788→/// Status of an analysis stage 789→enum StageStatus: String, Codable { 790→ case pending 791→ case inProgress 792→ case completed 793→ case failed 794→ case skipped 795→ 796→ var icon: String { 797→ switch self { 798→ case .pending: return "circle" 799→ case .inProgress: return "circle.dotted" 800→ case .completed: return "checkmark.circle.fill" 801→ case .failed: return "xmark.circle.fill" 802→ case .skipped: return "minus.circle" 803→ } 804→ } 805→ 806→ var color: Color { 807→ switch self { 808→ case .pending: return .secondary.opacity(0.5) 809→ case .inProgress: return .blue 810→ case .completed: return .green 811→ case .failed: return .red 812→ case .skipped: return .secondary 813→ } 814→ } 815→} 816→ 817→// MARK: - Performance Metrics 818→ 819→/// Real-time performance metrics during analysis 820→struct AnalysisPerformanceMetrics: Equatable { 821→ var articlesPerSecond: Double = 0 822→ var averageLatency: TimeInterval = 0 823→ var successRate: Double = 100 824→ var totalProcessed: Int = 0 825→ var successCount: Int = 0 826→ var errorCount: Int = 0 827→ var tokensUsed: Int = 0 828→ 829→ private var latencies: [TimeInterval] = [] 830→ private var startTime: Date? 831→ 832→ static var empty: AnalysisPerformanceMetrics { 833→ AnalysisPerformanceMetrics() 834→ } 835→ 836→ mutating func start() { 837→ startTime = Date() 838→ latencies = [] 839→ totalProcessed = 0 840→ successCount = 0 841→ errorCount = 0 842→ tokensUsed = 0 843→ } 844→ 845→ mutating func recordSuccess(latency: TimeInterval, tokens: Int = 0) { 846→ latencies.append(latency) 847→ totalProcessed += 1 848→ successCount += 1 849→ tokensUsed += tokens 850→ updateMetrics() 851→ } 852→ 853→ mutating func recordError() { 854→ totalProcessed += 1 855→ errorCount += 1 856→ updateMetrics() 857→ } 858→ 859→ private mutating func updateMetrics() { 860→ if !latencies.isEmpty { 861→ averageLatency = latencies.reduce(0, +) / Double(latencies.count) 862→ } 863→ if totalProcessed > 0 { 864→ successRate = Double(successCount) / Double(totalProcessed) * 100 865→ } 866→ if let start = startTime { 867→ let elapsed = Date().timeIntervalSince(start) 868→ if elapsed > 0 { 869→ articlesPerSecond = Double(totalProcessed) / elapsed 870→ } 871→ } 872→ } 873→ 874→ var formattedSpeed: String { String(format: "%.1f artigos/seg", articlesPerSecond) } 875→ var formattedLatency: String { String(format: "%.1fs", averageLatency) } 876→ var formattedSuccessRate: String { String(format: "%.1f%%", successRate) } 877→ var formattedTokens: String { 878→ tokensUsed >= 1000 ? String(format: "%.1fK", Double(tokensUsed) / 1000) : "\(tokensUsed)" 879→ } 880→} 881→ 882→// MARK: - Article Analysis Status 883→ 884→/// Status of an individual article during batch analysis 885→struct ArticleAnalysisStatus: Identifiable, Equatable { 886→ let id: String 887→ let title: String 888→ var status: ArticleStatus 889→ var sentiment: String? 890→ var duration: TimeInterval? 891→ var errorMessage: String? 892→ 893→ enum ArticleStatus: String { 894→ case pending 895→ case analyzing 896→ case completed 897→ case failed 898→ } 899→ 900→ var statusIcon: String { 901→ switch status { 902→ case .pending: return "circle" 903→ case .analyzing: return "circle.dotted" 904→ case .completed: return "checkmark.circle.fill" 905→ case .failed: return "xmark.circle.fill" 906→ } 907→ } 908→ 909→ var statusColor: Color { 910→ switch status { 911→ case .pending: return .secondary.opacity(0.5) 912→ case .analyzing: return .blue 913→ case .completed: return .green 914→ case .failed: return .red 915→ } 916→ } 917→ 918→ var formattedDuration: String? { 919→ guard let duration = duration else { return nil } 920→ return String(format: "%.1fs", duration) 921→ } 922→} 923→ 924→// MARK: - Default Pipeline Stages 925→ 926→extension AnalysisStage { 927→ static func defaultPipeline() -> [AnalysisStage] { 928→ [ 929→ AnalysisStage(id: "connect", name: "Conectando ao servidor", status: .pending), 930→ AnalysisStage(id: "prepare", name: "Preparando artigos", status: .pending), 931→ AnalysisStage(id: "send", name: "Enviando para LM Studio", status: .pending), 932→ AnalysisStage(id: "process", name: "Processando resposta", status: .pending), 933→ AnalysisStage(id: "save", name: "Salvando resultados", status: .pending) 934→ ] 935→ } 936→} 937→ 938→// MARK: - Timeline Data Types 939→ 940→/// Data point for sentiment timeline chart 941→struct TimelineDataPoint: Identifiable, Sendable { 942→ let id = UUID() 943→ let date: Date 944→ let averageScore: Double 945→ let articleCount: Int 946→ let keyword: String 947→} 948→ 949→/// Annotation for significant events in timeline 950→struct SentimentAnnotation: Identifiable, Sendable { 951→ let id = UUID() 952→ let date: Date 953→ let type: AnnotationType 954→ let description: String 955→ 956→ enum AnnotationType: String, Sendable { 957→ case spike = "spike" 958→ case drop = "drop" 959→ case highVolume = "highVolume" 960→ 961→ var icon: String { 962→ switch self { 963→ case .spike: return "arrow.up.circle.fill" 964→ case .drop: return "arrow.down.circle.fill" 965→ case .highVolume: return "chart.bar.fill" 966→ } 967→ } 968→ 969→ var color: Color { 970→ switch self { 971→ case .spike: return .green 972→ case .drop: return .red 973→ case .highVolume: return .blue 974→ } 975→ } 976→ } 977→} 978→ 979→/// Time period for timeline display 980→enum TimelinePeriod: String, CaseIterable, Identifiable, Sendable { 981→ case week = "7 dias" 982→ case twoWeeks = "14 dias" 983→ case month = "30 dias" 984→ 985→ var id: String { rawValue } 986→ 987→ var days: Int { 988→ switch self { 989→ case .week: return 7 990→ case .twoWeeks: return 14 991→ case .month: return 30 992→ } 993→ } 994→} 995→ 996→/// Summary statistics for timeline data 997→struct TimelineSummary: Sendable { 998→ let averageSentiment: Double 999→ let minSentiment: Double 1000→ let maxSentiment: Double 1001→ let totalArticles: Int 1002→ let trendDirection: TrendDirection 1003→ let volatility: Double 1004→ 1005→ enum TrendDirection: String, Sendable { 1006→ case improving = "improving" 1007→ case declining = "declining" 1008→ case stable = "stable" 1009→ 1010→ var icon: String { 1011→ switch self { 1012→ case .improving: return "arrow.up.right" 1013→ case .declining: return "arrow.down.right" 1014→ case .stable: return "arrow.right" 1015→ } 1016→ } 1017→ 1018→ var color: Color { 1019→ switch self { 1020→ case .improving: return .green 1021→ case .declining: return .red 1022→ case .stable: return .gray 1023→ } 1024→ } 1025→ 1026→ var label: String { 1027→ switch self { 1028→ case .improving: return "Melhorando" 1029→ case .declining: return "Declinando" 1030→ case .stable: return "Estável" 1031→ } 1032→ } 1033→ } 1034→ 1035→ static let empty = TimelineSummary( 1036→ averageSentiment: 0, 1037→ minSentiment: 0, 1038→ maxSentiment: 0, 1039→ totalArticles: 0, 1040→ trendDirection: .stable, 1041→ volatility: 0 1042→ ) 1043→} 1044→ 1045→// MARK: - Analysis Checkpoint 1046→ 1047→/// Checkpoint for resumable batch analysis 1048→struct AnalysisCheckpoint: Codable, Sendable { 1049→ var articleIds: [String] 1050→ var completedIds: Set 1051→ var startedAt: Date 1052→ var lastSaveAt: Date 1053→ 1054→ var completedCount: Int { completedIds.count } 1055→ var pendingIds: [String] { articleIds.filter { !completedIds.contains($0) } } 1056→ var progress: Double { 1057→ guard !articleIds.isEmpty else { return 0 } 1058→ return Double(completedCount) / Double(articleIds.count) 1059→ } 1060→} 1061→ 1062→// MARK: - Cached Analysis Stats 1063→ 1064→/// Cached statistics for optimized display 1065→struct CachedAnalysisStats: Sendable { 1066→ let timestamp: Date 1067→ let positiveCount: Int 1068→ let negativeCount: Int 1069→ let neutralCount: Int 1070→ let categoryDistribution: [String: Int] 1071→ let sourceStats: [(source: String, count: Int, avgSentiment: Double)] 1072→ 1073→ var isValid: Bool { 1074→ Date().timeIntervalSince(timestamp) < 60 1075→ } 1076→ 1077→ var totalAnalyzed: Int { 1078→ positiveCount + negativeCount + neutralCount 1079→ } 1080→ 1081→ static let empty = CachedAnalysisStats( 1082→ timestamp: Date.distantPast, 1083→ positiveCount: 0, 1084→ negativeCount: 0, 1085→ neutralCount: 0, 1086→ categoryDistribution: [:], 1087→ sourceStats: [] 1088→ ) 1089→} 1090→ 1091→// MARK: - Analysis Phase 1092→ 1093→/// Current phase of batch analysis 1094→enum AnalysisPhase: String, Sendable { 1095→ case idle = "Aguardando" 1096→ case preparing = "Preparando" 1097→ case analyzing = "Analisando" 1098→ case saving = "Salvando" 1099→ case completed = "Concluído" 1100→ case failed = "Falhou" 1101→ case cancelled = "Cancelado" 1102→ 1103→ var isActive: Bool { 1104→ switch self { 1105→ case .preparing, .analyzing, .saving: return true 1106→ default: return false 1107→ } 1108→ } 1109→} 1110→ 1111→// MARK: - Analysis Progress 1112→ 1113→/// Progress information for UI display 1114→struct AnalysisProgress: Sendable { 1115→ let currentIndex: Int 1116→ let totalCount: Int 1117→ let estimatedTimeRemaining: TimeInterval 1118→ let phase: AnalysisPhase 1119→ 1120→ var progress: Double { 1121→ guard totalCount > 0 else { return 0 } 1122→ return Double(currentIndex) / Double(totalCount) 1123→ } 1124→ 1125→ var formattedTimeRemaining: String { 1126→ guard estimatedTimeRemaining > 0 else { return "--" } 1127→ let minutes = Int(estimatedTimeRemaining / 60) 1128→ let seconds = Int(estimatedTimeRemaining) % 60 1129→ return minutes > 0 ? "\(minutes)m \(seconds)s" : "\(seconds)s" 1130→ } 1131→ 1132→ static let idle = AnalysisProgress( 1133→ currentIndex: 0, 1134→ totalCount: 0, 1135→ estimatedTimeRemaining: 0, 1136→ phase: .idle 1137→ ) 1138→} 1139→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.