/ RacerTracer / Views / KeywordAnalysisView.swift
KeywordAnalysisView.swift
  1  //
  2  //  KeywordAnalysisView.swift
  3  //  RacerTracer
  4  //
  5  //  Created by Alexander Kunau on 29.09.25.
  6  //
  7  
  8  import SwiftUI
  9  import Charts
 10  
 11  struct KeywordAnalysisView: View {
 12      @EnvironmentObject var seoService: SEOAnalysisService
 13      @State private var targetKeywords = ""
 14      @State private var analysisResults: KeywordAnalysisResults?
 15      @State private var isAnalyzing = false
 16      @State private var selectedMetric: KeywordMetricType = .frequency
 17      
 18      var body: some View {
 19          VStack(spacing: 24) {
 20              // Header with Input
 21              KeywordInputSection(
 22                  targetKeywords: $targetKeywords,
 23                  onAnalyze: performKeywordAnalysis,
 24                  isAnalyzing: isAnalyzing
 25              )
 26              
 27              if isAnalyzing {
 28                  KeywordAnalysisProgressView()
 29              } else if let results = analysisResults {
 30                  KeywordResultsView(
 31                      results: results,
 32                      selectedMetric: $selectedMetric
 33                  )
 34              } else {
 35                  KeywordStartView()
 36              }
 37          }
 38          .padding(24)
 39          .navigationTitle("Keyword Analysis")
 40      }
 41      
 42      private func performKeywordAnalysis() {
 43          guard !targetKeywords.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
 44          
 45          isAnalyzing = true
 46          
 47          // Verwende die letzte Analyse wenn verfügbar, sonst simuliere Daten
 48          if let lastAnalysis = seoService.currentAnalysis {
 49              DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
 50                  analysisResults = extractKeywordResults(from: lastAnalysis, targetKeywords: targetKeywords)
 51                  isAnalyzing = false
 52              }
 53          } else {
 54              // Generiere Demo-Daten für Keyword-Analyse
 55              DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
 56                  analysisResults = generateDemoKeywordResults(targetKeywords: targetKeywords)
 57                  isAnalyzing = false
 58              }
 59          }
 60      }
 61      
 62      private func extractKeywordResults(from analysis: SEOAnalysis, targetKeywords: String) -> KeywordAnalysisResults {
 63          let keywords = targetKeywords.components(separatedBy: ",")
 64              .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
 65              .filter { !$0.isEmpty }
 66          
 67          return KeywordAnalysisResults(
 68              targetKeywords: keywords,
 69              foundKeywords: analysis.metrics.keywords.primaryKeywords + 
 70                            analysis.metrics.keywords.secondaryKeywords,
 71              keywordDensity: analysis.metrics.keywords.keywordDensity,
 72              keywordDistribution: analysis.metrics.keywords.keywordDistribution,
 73              tfIdfScores: analysis.metrics.keywords.tfIdfScores,
 74              competitorData: generateMockCompetitorData(for: keywords),
 75              suggestions: generateKeywordSuggestions(from: analysis.metrics.keywords)
 76          )
 77      }
 78      
 79      private func generateDemoKeywordResults(targetKeywords: String) -> KeywordAnalysisResults {
 80          let keywords = targetKeywords.components(separatedBy: ",")
 81              .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
 82              .filter { !$0.isEmpty }
 83          
 84          let demoKeywords = keywords.map { keyword in
 85              KeywordMetric(
 86                  keyword: keyword,
 87                  frequency: Int.random(in: 5...25),
 88                  density: Double.random(in: 0.005...0.03),
 89                  prominence: Double.random(in: 0.3...0.9),
 90                  contexts: ["title", "h1", "content"]
 91              )
 92          }
 93          
 94          let demoDistribution = keywords.map { keyword in
 95              KeywordDistribution(
 96                  keyword: keyword,
 97                  titlePresence: Bool.random(),
 98                  h1Presence: Bool.random(),
 99                  h2Presence: Bool.random(),
100                  metaDescriptionPresence: Bool.random(),
101                  contentPresence: true,
102                  urlPresence: Bool.random()
103              )
104          }
105          
106          let demoTFIDF = keywords.map { keyword in
107              TFIDFScore(
108                  term: keyword,
109                  tfIdf: Double.random(in: 0.1...2.5),
110                  termFrequency: Double.random(in: 0.001...0.02),
111                  inverseDocumentFrequency: Double.random(in: 1.5...4.0)
112              )
113          }
114          
115          return KeywordAnalysisResults(
116              targetKeywords: keywords,
117              foundKeywords: demoKeywords,
118              keywordDensity: Double.random(in: 0.01...0.05),
119              keywordDistribution: demoDistribution,
120              tfIdfScores: demoTFIDF,
121              competitorData: generateMockCompetitorData(for: keywords),
122              suggestions: generateDemoKeywordSuggestions(for: keywords)
123          )
124      }
125  }
126  
127  // MARK: - Supporting Views
128  
129  struct KeywordInputSection: View {
130      @Binding var targetKeywords: String
131      let onAnalyze: () -> Void
132      let isAnalyzing: Bool
133      
134      var body: some View {
135          VStack(alignment: .leading, spacing: 16) {
136              Text("Keyword Research & Analysis")
137                  .font(.title2)
138                  .fontWeight(.semibold)
139              
140              Text("Analyze keyword performance, density, and competition. Enter keywords separated by commas.")
141                  .font(.subheadline)
142                  .foregroundColor(.secondary)
143              
144              HStack(spacing: 12) {
145                  TextField("Enter keywords (e.g., SEO, website optimization, search ranking)", text: $targetKeywords)
146                      .textFieldStyle(RoundedBorderTextFieldStyle())
147                      .disabled(isAnalyzing)
148                  
149                  Button(action: onAnalyze) {
150                      HStack {
151                          if isAnalyzing {
152                              ProgressView()
153                                  .scaleEffect(0.8)
154                          } else {
155                              Image(systemName: "magnifyingglass")
156                          }
157                          Text(isAnalyzing ? "Analyzing..." : "Analyze")
158                      }
159                  }
160                  .buttonStyle(.borderedProminent)
161                  .disabled(targetKeywords.isEmpty || isAnalyzing)
162              }
163          }
164          .padding(20)
165          .background(Color(NSColor.controlBackgroundColor))
166          .cornerRadius(12)
167      }
168  }
169  
170  struct KeywordAnalysisProgressView: View {
171      var body: some View {
172          VStack(spacing: 20) {
173              ProgressView()
174                  .scaleEffect(1.5)
175              
176              Text("Analyzing Keywords...")
177                  .font(.headline)
178                  .foregroundColor(.secondary)
179              
180              Text("Calculating keyword density, distribution, and TF-IDF scores")
181                  .font(.subheadline)
182                  .foregroundColor(.secondary)
183                  .multilineTextAlignment(.center)
184          }
185          .frame(maxWidth: .infinity)
186          .frame(height: 200)
187          .background(Color(NSColor.controlBackgroundColor))
188          .cornerRadius(12)
189      }
190  }
191  
192  struct KeywordStartView: View {
193      var body: some View {
194          VStack(spacing: 20) {
195              Image(systemName: "key.fill")
196                  .font(.system(size: 48))
197                  .foregroundColor(.accentColor)
198              
199              Text("Keyword Analysis")
200                  .font(.title2)
201                  .fontWeight(.semibold)
202              
203              Text("Enter your target keywords to analyze their performance, density, distribution, and competition.")
204                  .font(.subheadline)
205                  .foregroundColor(.secondary)
206                  .multilineTextAlignment(.center)
207          }
208          .frame(maxWidth: .infinity, maxHeight: .infinity)
209          .background(Color(NSColor.controlBackgroundColor))
210          .cornerRadius(12)
211      }
212  }
213  
214  enum KeywordMetricType: String, CaseIterable {
215      case frequency = "Frequency"
216      case density = "Density"
217      case prominence = "Prominence"
218      case tfIdf = "TF-IDF"
219  }
220  
221  struct KeywordResultsView: View {
222      let results: KeywordAnalysisResults
223      @Binding var selectedMetric: KeywordMetricType
224      
225      var body: some View {
226          ScrollView {
227              VStack(spacing: 24) {
228                  // Overview Cards
229                  KeywordOverviewCards(results: results)
230                  
231                  // Metrics Chart
232                  KeywordMetricsChart(
233                      keywords: results.foundKeywords,
234                      selectedMetric: selectedMetric
235                  )
236                  
237                  // Metric Selector
238                  Picker("Metric", selection: $selectedMetric) {
239                      ForEach(KeywordMetricType.allCases, id: \.self) { metric in
240                          Text(metric.rawValue).tag(metric)
241                      }
242                  }
243                  .pickerStyle(SegmentedPickerStyle())
244                  
245                  // Keyword Distribution
246                  KeywordDistributionSection(distribution: results.keywordDistribution)
247                  
248                  // TF-IDF Analysis
249                  TFIDFAnalysisSection(scores: results.tfIdfScores)
250                  
251                  // Competition Analysis
252                  CompetitionAnalysisSection(competitorData: results.competitorData)
253                  
254                  // Suggestions
255                  KeywordSuggestionsSection(suggestions: results.suggestions)
256              }
257              .padding(20)
258          }
259          .background(Color(NSColor.controlBackgroundColor))
260          .cornerRadius(12)
261      }
262  }
263  
264  struct KeywordOverviewCards: View {
265      let results: KeywordAnalysisResults
266      
267      var body: some View {
268          HStack(spacing: 16) {
269              KeywordOverviewCard(
270                  title: "Keywords Found",
271                  value: "\(results.foundKeywords.count)",
272                  subtitle: "of \(results.targetKeywords.count) targets",
273                  color: .blue
274              )
275              
276              KeywordOverviewCard(
277                  title: "Avg. Density",
278                  value: String(format: "%.2f%%", results.keywordDensity * 100),
279                  subtitle: "keyword density",
280                  color: .green
281              )
282              
283              KeywordOverviewCard(
284                  title: "Avg. TF-IDF",
285                  value: String(format: "%.2f", results.tfIdfScores.map(\.tfIdf).averageValue),
286                  subtitle: "relevance score",
287                  color: .orange
288              )
289          }
290      }
291  }
292  
293  struct KeywordOverviewCard: View {
294      let title: String
295      let value: String
296      let subtitle: String
297      let color: Color
298      
299      var body: some View {
300          VStack(spacing: 8) {
301              Text(title)
302                  .font(.caption)
303                  .foregroundColor(.secondary)
304              
305              Text(value)
306                  .font(.title)
307                  .fontWeight(.bold)
308                  .foregroundColor(color)
309              
310              Text(subtitle)
311                  .font(.caption2)
312                  .foregroundColor(.secondary)
313          }
314          .frame(maxWidth: .infinity)
315          .padding(16)
316          .background(Color(NSColor.textBackgroundColor))
317          .cornerRadius(8)
318      }
319  }
320  
321  struct KeywordMetricsChart: View {
322      let keywords: [KeywordMetric]
323      let selectedMetric: KeywordMetricType
324      
325      var body: some View {
326          VStack(alignment: .leading, spacing: 16) {
327              Text("\(selectedMetric.rawValue) by Keyword")
328                  .font(.headline)
329                  .fontWeight(.semibold)
330              
331              Chart(keywords.prefix(10), id: \.keyword) { keyword in
332                  BarMark(
333                      x: .value("Keyword", keyword.keyword),
334                      y: .value(selectedMetric.rawValue, metricValue(for: keyword))
335                  )
336                  .foregroundStyle(Color.accentColor.gradient)
337              }
338              .frame(height: 200)
339              .chartXAxis {
340                  AxisMarks { value in
341                      AxisValueLabel() {
342                          if let keyword = value.as(String.self) {
343                              Text(keyword)
344                                  .rotationEffect(.degrees(-45))
345                                  .font(.caption)
346                          }
347                      }
348                  }
349              }
350          }
351      }
352      
353      private func metricValue(for keyword: KeywordMetric) -> Double {
354          switch selectedMetric {
355          case .frequency:
356              return Double(keyword.frequency)
357          case .density:
358              return keyword.density * 100
359          case .prominence:
360              return keyword.prominence * 100
361          case .tfIdf:
362              return keyword.prominence // Placeholder, würde TF-IDF aus separater Liste nehmen
363          }
364      }
365  }
366  
367  struct KeywordDistributionSection: View {
368      let distribution: [KeywordDistribution]
369      
370      var body: some View {
371          VStack(alignment: .leading, spacing: 16) {
372              Text("Keyword Distribution")
373                  .font(.headline)
374                  .fontWeight(.semibold)
375              
376              LazyVStack(spacing: 12) {
377                  ForEach(distribution.prefix(8), id: \.id) { item in
378                      KeywordDistributionItemRow(distribution: item)
379                  }
380              }
381          }
382      }
383  }
384  
385  struct KeywordDistributionItemRow: View {
386      let distribution: KeywordDistribution
387      
388      var body: some View {
389          VStack(alignment: .leading, spacing: 8) {
390              Text(distribution.keyword)
391                  .font(.subheadline)
392                  .fontWeight(.medium)
393              
394              HStack(spacing: 12) {
395                  KeywordDistributionIndicator(label: "Title", isPresent: distribution.titlePresence)
396                  KeywordDistributionIndicator(label: "H1", isPresent: distribution.h1Presence)
397                  KeywordDistributionIndicator(label: "H2", isPresent: distribution.h2Presence)
398                  KeywordDistributionIndicator(label: "Meta", isPresent: distribution.metaDescriptionPresence)
399                  KeywordDistributionIndicator(label: "Content", isPresent: distribution.contentPresence)
400                  KeywordDistributionIndicator(label: "URL", isPresent: distribution.urlPresence)
401                  
402                  Spacer()
403              }
404          }
405          .padding(12)
406          .background(Color(NSColor.textBackgroundColor))
407          .cornerRadius(8)
408      }
409  }
410  
411  struct KeywordDistributionIndicator: View {
412      let label: String
413      let isPresent: Bool
414      
415      var body: some View {
416          VStack(spacing: 4) {
417              Circle()
418                  .fill(isPresent ? Color.green : Color.secondary.opacity(0.3))
419                  .frame(width: 8, height: 8)
420              
421              Text(label)
422                  .font(.caption2)
423                  .foregroundColor(.secondary)
424          }
425      }
426  }
427  
428  struct TFIDFAnalysisSection: View {
429      let scores: [TFIDFScore]
430      
431      var body: some View {
432          VStack(alignment: .leading, spacing: 16) {
433              Text("TF-IDF Analysis")
434                  .font(.headline)
435                  .fontWeight(.semibold)
436              
437              Text("Term Frequency-Inverse Document Frequency scores indicating keyword relevance")
438                  .font(.caption)
439                  .foregroundColor(.secondary)
440              
441              LazyVStack(spacing: 8) {
442                  ForEach(scores.sorted { $0.tfIdf > $1.tfIdf }.prefix(8), id: \.id) { score in
443                      KeywordTFIDFScoreRow(score: score)
444                  }
445              }
446          }
447      }
448  }
449  
450  struct KeywordTFIDFScoreRow: View {
451      let score: TFIDFScore
452      
453      var body: some View {
454          HStack {
455              VStack(alignment: .leading, spacing: 2) {
456                  Text(score.term)
457                      .font(.subheadline)
458                      .fontWeight(.medium)
459                  
460                  HStack(spacing: 16) {
461                      Text("TF: \(String(format: "%.3f", score.termFrequency))")
462                          .font(.caption)
463                          .foregroundColor(.secondary)
464                      
465                      Text("IDF: \(String(format: "%.3f", score.inverseDocumentFrequency))")
466                          .font(.caption)
467                          .foregroundColor(.secondary)
468                  }
469              }
470              
471              Spacer()
472              
473              VStack(alignment: .trailing, spacing: 2) {
474                  Text(String(format: "%.3f", score.tfIdf))
475                      .font(.title3)
476                      .fontWeight(.semibold)
477                      .foregroundColor(.accentColor)
478                  
479                  Text("Relevance")
480                      .font(.caption2)
481                      .foregroundColor(.secondary)
482              }
483          }
484          .padding(12)
485          .background(Color(NSColor.textBackgroundColor))
486          .cornerRadius(8)
487      }
488  }
489  
490  struct CompetitionAnalysisSection: View {
491      let competitorData: [CompetitorKeywordData]
492      
493      var body: some View {
494          VStack(alignment: .leading, spacing: 16) {
495              Text("Competition Analysis")
496                  .font(.headline)
497                  .fontWeight(.semibold)
498              
499              LazyVStack(spacing: 12) {
500                  ForEach(competitorData, id: \.keyword) { data in
501                      CompetitorKeywordRow(data: data)
502                  }
503              }
504          }
505      }
506  }
507  
508  struct CompetitorKeywordRow: View {
509      let data: CompetitorKeywordData
510      
511      var body: some View {
512          HStack {
513              VStack(alignment: .leading, spacing: 4) {
514                  Text(data.keyword)
515                      .font(.subheadline)
516                      .fontWeight(.medium)
517                  
518                  Text("Competition Level: \(data.competitionLevel)")
519                      .font(.caption)
520                      .foregroundColor(.secondary)
521              }
522              
523              Spacer()
524              
525              VStack(alignment: .trailing, spacing: 4) {
526                  Text("Difficulty: \(data.difficulty)/10")
527                      .font(.caption)
528                      .foregroundColor(difficultyColor(data.difficulty))
529                  
530                  Text("\(data.monthlySearches) searches/month")
531                      .font(.caption2)
532                      .foregroundColor(.secondary)
533              }
534          }
535          .padding(12)
536          .background(Color(NSColor.textBackgroundColor))
537          .cornerRadius(8)
538      }
539      
540      private func difficultyColor(_ difficulty: Int) -> Color {
541          switch difficulty {
542          case 1...3: return .green
543          case 4...6: return .orange
544          case 7...10: return .red
545          default: return .secondary
546          }
547      }
548  }
549  
550  struct KeywordSuggestionsSection: View {
551      let suggestions: [KeywordSuggestion]
552      
553      var body: some View {
554          VStack(alignment: .leading, spacing: 16) {
555              Text("Keyword Suggestions")
556                  .font(.headline)
557                  .fontWeight(.semibold)
558              
559              LazyVStack(spacing: 12) {
560                  ForEach(suggestions, id: \.keyword) { suggestion in
561                      KeywordSuggestionRow(suggestion: suggestion)
562                  }
563              }
564          }
565      }
566  }
567  
568  struct KeywordSuggestionRow: View {
569      let suggestion: KeywordSuggestion
570      
571      var body: some View {
572          HStack {
573              VStack(alignment: .leading, spacing: 4) {
574                  Text(suggestion.keyword)
575                      .font(.subheadline)
576                      .fontWeight(.medium)
577                  
578                  Text(suggestion.reason)
579                      .font(.caption)
580                      .foregroundColor(.secondary)
581              }
582              
583              Spacer()
584              
585              VStack(alignment: .trailing, spacing: 4) {
586                  Text("Potential: \(suggestion.potential)")
587                      .font(.caption)
588                      .foregroundColor(.accentColor)
589                      .fontWeight(.medium)
590                  
591                  Text("\(suggestion.searchVolume) vol/month")
592                      .font(.caption2)
593                      .foregroundColor(.secondary)
594              }
595          }
596          .padding(12)
597          .background(Color(NSColor.textBackgroundColor))
598          .cornerRadius(8)
599      }
600  }
601  
602  // MARK: - Data Models
603  
604  struct KeywordAnalysisResults {
605      let targetKeywords: [String]
606      let foundKeywords: [KeywordMetric]
607      let keywordDensity: Double
608      let keywordDistribution: [KeywordDistribution]
609      let tfIdfScores: [TFIDFScore]
610      let competitorData: [CompetitorKeywordData]
611      let suggestions: [KeywordSuggestion]
612  }
613  
614  struct CompetitorKeywordData {
615      let keyword: String
616      let competitionLevel: String
617      let difficulty: Int
618      let monthlySearches: Int
619  }
620  
621  struct KeywordSuggestion {
622      let keyword: String
623      let reason: String
624      let potential: String
625      let searchVolume: Int
626  }
627  
628  // MARK: - Helper Functions
629  
630  private func generateMockCompetitorData(for keywords: [String]) -> [CompetitorKeywordData] {
631      return keywords.map { keyword in
632          CompetitorKeywordData(
633              keyword: keyword,
634              competitionLevel: ["Low", "Medium", "High"].randomElement() ?? "Medium",
635              difficulty: Int.random(in: 1...10),
636              monthlySearches: Int.random(in: 100...10000)
637          )
638      }
639  }
640  
641  private func generateKeywordSuggestions(from keywordAnalysis: KeywordAnalysis) -> [KeywordSuggestion] {
642      let baseKeywords = keywordAnalysis.primaryKeywords.map(\.keyword)
643      return baseKeywords.flatMap { keyword in
644          [
645              KeywordSuggestion(
646                  keyword: "long tail \(keyword)",
647                  reason: "Lower competition, higher intent",
648                  potential: "High",
649                  searchVolume: Int.random(in: 50...500)
650              ),
651              KeywordSuggestion(
652                  keyword: "\(keyword) guide",
653                  reason: "Informational content opportunity",
654                  potential: "Medium",
655                  searchVolume: Int.random(in: 200...2000)
656              )
657          ]
658      }.prefix(6).map { $0 }
659  }
660  
661  private func generateDemoKeywordSuggestions(for keywords: [String]) -> [KeywordSuggestion] {
662      return keywords.flatMap { keyword in
663          [
664              KeywordSuggestion(
665                  keyword: "best \(keyword)",
666                  reason: "High commercial intent",
667                  potential: "High",
668                  searchVolume: Int.random(in: 500...5000)
669              ),
670              KeywordSuggestion(
671                  keyword: "\(keyword) tutorial",
672                  reason: "Educational content opportunity",
673                  potential: "Medium",
674                  searchVolume: Int.random(in: 200...2000)
675              )
676          ]
677      }.prefix(8).map { $0 }
678  }
679  
680  
681  
682  #Preview {
683      KeywordAnalysisView()
684          .environmentObject(SEOAnalysisService())
685  }