/ RacerTracer / Views / JSAnalysisView.swift
JSAnalysisView.swift
  1  //
  2  //  JSAnalysisView.swift
  3  //  RacerTracer
  4  //
  5  //  Created by Alexander Kunau on 29.09.25.
  6  //
  7  
  8  import SwiftUI
  9  import Charts
 10  
 11  struct JSAnalysisView: View {
 12      @StateObject private var analysisService = JSAnalysisService()
 13      @EnvironmentObject private var settingsService: SettingsService
 14      @State private var urlToAnalyze = ""
 15      @State private var selectedFilter = "All Files"
 16      @State private var selectedIssue = "All Issues"
 17      
 18      var body: some View {
 19          NavigationView {
 20              VStack(spacing: 0) {
 21                  // Header Section
 22                  VStack(alignment: .leading, spacing: 16) {
 23                      Text("JavaScript Analysis")
 24                          .font(.title2)
 25                          .fontWeight(.semibold)
 26                      
 27                      Text("Analyze JavaScript files for performance impact, loading behavior, execution time, and SEO optimization.")
 28                          .font(.subheadline)
 29                          .foregroundColor(.secondary)
 30                  }
 31                  .frame(maxWidth: .infinity, alignment: .leading)
 32                  .padding(24)
 33                  .background(Color(NSColor.controlBackgroundColor))
 34                  
 35                  // Analysis Input Section
 36                  VStack(spacing: 16) {
 37                      HStack {
 38                          TextField("Enter website URL to analyze JavaScript files", text: $urlToAnalyze)
 39                              .textFieldStyle(RoundedBorderTextFieldStyle())
 40                          
 41                          Button(action: {
 42                              Task {
 43                                  await analysisService.analyzeJavaScript(url: urlToAnalyze)
 44                              }
 45                          }) {
 46                              if analysisService.isAnalyzing {
 47                                  ProgressView()
 48                                      .scaleEffect(0.8)
 49                              } else {
 50                                  Text("Analyze")
 51                              }
 52                          }
 53                          .disabled(urlToAnalyze.isEmpty || analysisService.isAnalyzing)
 54                          .buttonStyle(.borderedProminent)
 55                      }
 56                      
 57                      if analysisService.isAnalyzing {
 58                          ProgressView("Analyzing JavaScript performance...", value: analysisService.progress, total: 1.0)
 59                              .progressViewStyle(LinearProgressViewStyle())
 60                      }
 61                  }
 62                  .padding(.horizontal, 24)
 63                  .padding(.bottom, 16)
 64                  
 65                  // Content Area
 66                  ScrollView {
 67                      if let analysis = analysisService.currentAnalysis {
 68                          VStack(spacing: 24) {
 69                              // Overview Cards
 70                              JSOverviewCardsSection(analysis: analysis)
 71                              
 72                              // Filters
 73                              JSFiltersSection(
 74                                  selectedFilter: $selectedFilter,
 75                                  selectedIssue: $selectedIssue
 76                              )
 77                              
 78                              // Performance Impact Charts
 79                              JSPerformanceChartsSection(analysis: analysis)
 80                              
 81                              // Critical JS Issues Section
 82                              if !analysis.criticalIssues.isEmpty {
 83                                  CriticalJSIssuesSection(issues: analysis.criticalIssues)
 84                              }
 85                              
 86                              // Large JavaScript Files Section
 87                              if !analysis.largeJSFiles.isEmpty {
 88                                  LargeJSFilesSection(files: analysis.largeJSFiles)
 89                              }
 90                              
 91                              // Render Blocking JS Section
 92                              if !analysis.renderBlockingJS.isEmpty {
 93                                  RenderBlockingJSSection(files: analysis.renderBlockingJS)
 94                              }
 95                              
 96                              // Third-party Scripts Section
 97                              if !analysis.thirdPartyScripts.isEmpty {
 98                                  ThirdPartyScriptsSection(scripts: analysis.thirdPartyScripts)
 99                              }
100                              
101                              // JavaScript Files List
102                              JSFilesListSection(
103                                  files: filteredJSFiles(analysis.jsFiles),
104                                  selectedFilter: selectedFilter,
105                                  selectedIssue: selectedIssue
106                              )
107                              
108                              // Performance & SEO Recommendations
109                              JSRecommendationsSection(analysis: analysis)
110                          }
111                          .padding(24)
112                      } else if !analysisService.isAnalyzing {
113                          JSEmptyStateView()
114                      }
115                  }
116              }
117          }
118          .navigationTitle("JavaScript Analysis")
119      }
120      
121      private func filteredJSFiles(_ files: [JSFile]) -> [JSFile] {
122          var filtered = files
123          
124          switch selectedFilter {
125          case "External Only":
126              filtered = filtered.filter { $0.isExternal }
127          case "Internal Only":
128              filtered = filtered.filter { !$0.isExternal }
129          case "Large Files":
130              filtered = filtered.filter { $0.fileSize > 100000 } // > 100KB
131          case "Render Blocking":
132              filtered = filtered.filter { $0.isRenderBlocking }
133          case "Third-party":
134              filtered = filtered.filter { $0.isThirdParty }
135          default:
136              break
137          }
138          
139          switch selectedIssue {
140          case "Slow Execution":
141              filtered = filtered.filter { $0.executionTime > 100 } // > 100ms
142          case "Large Size":
143              filtered = filtered.filter { $0.fileSize > 100000 }
144          case "Blocking Render":
145              filtered = filtered.filter { $0.isRenderBlocking }
146          case "Memory Issues":
147              filtered = filtered.filter { $0.memoryUsage > 1000000 } // > 1MB
148          case "No Issues":
149              filtered = filtered.filter { $0.fileSize <= 100000 && $0.executionTime <= 100 && !$0.isRenderBlocking }
150          default:
151              break
152          }
153          
154          return filtered
155      }
156  }
157  
158  // MARK: - JS Overview Cards Section
159  
160  struct JSOverviewCardsSection: View {
161      let analysis: JSAnalysis
162      
163      var body: some View {
164          VStack(alignment: .leading, spacing: 16) {
165              Text("JavaScript Performance Overview")
166                  .font(.headline)
167                  .fontWeight(.semibold)
168              
169              LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) {
170                  OverviewCard(
171                      title: "Total JS Files",
172                      value: "\(analysis.totalJSFiles)",
173                      subtitle: "Found on website",
174                      color: .blue,
175                      icon: "curlybraces"
176                  )
177                  
178                  OverviewCard(
179                      title: "Total JS Size",
180                      value: formatFileSize(analysis.totalJSSize),
181                      subtitle: "Combined size",
182                      color: analysis.totalJSSize < 200000 ? .green : analysis.totalJSSize < 500000 ? .orange : .red,
183                      icon: "tray.full"
184                  )
185                  
186                  OverviewCard(
187                      title: "Execution Time",
188                      value: "\(Int(analysis.totalExecutionTime))ms",
189                      subtitle: "Total execution",
190                      color: analysis.totalExecutionTime < 500 ? .green : analysis.totalExecutionTime < 1000 ? .orange : .red,
191                      icon: "clock"
192                  )
193                  
194                  OverviewCard(
195                      title: "Render Blocking",
196                      value: "\(analysis.renderBlockingFiles)",
197                      subtitle: "Files blocking render",
198                      color: analysis.renderBlockingFiles == 0 ? .green : analysis.renderBlockingFiles < 3 ? .orange : .red,
199                      icon: "exclamationmark.triangle"
200                  )
201              }
202              
203              // Additional metrics row
204              LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) {
205                  OverviewCard(
206                      title: "Third-party Scripts",
207                      value: "\(analysis.thirdPartyScripts.count)",
208                      subtitle: "External scripts",
209                      color: .purple,
210                      icon: "globe"
211                  )
212                  
213                  OverviewCard(
214                      title: "Memory Usage",
215                      value: formatMemorySize(analysis.totalMemoryUsage),
216                      subtitle: "Heap memory",
217                      color: analysis.totalMemoryUsage < 5000000 ? .green : analysis.totalMemoryUsage < 10000000 ? .orange : .red,
218                      icon: "memorychip"
219                  )
220                  
221                  OverviewCard(
222                      title: "Async Scripts",
223                      value: "\(analysis.asyncScripts)",
224                      subtitle: "Non-blocking",
225                      color: .green,
226                      icon: "arrow.clockwise"
227                  )
228                  
229                  OverviewCard(
230                      title: "Performance Score",
231                      value: String(format: "%.0f%%", analysis.performanceScore * 100),
232                      subtitle: "JS optimization",
233                      color: analysis.performanceScore > 0.8 ? .green : analysis.performanceScore > 0.6 ? .orange : .red,
234                      icon: "checkmark.shield"
235                  )
236              }
237          }
238      }
239      
240      private func formatFileSize(_ bytes: Int) -> String {
241          let formatter = ByteCountFormatter()
242          formatter.allowedUnits = [.useKB, .useMB]
243          formatter.countStyle = .file
244          return formatter.string(fromByteCount: Int64(bytes))
245      }
246      
247      private func formatMemorySize(_ bytes: Int) -> String {
248          let formatter = ByteCountFormatter()
249          formatter.allowedUnits = [.useMB]
250          formatter.countStyle = .memory
251          return formatter.string(fromByteCount: Int64(bytes))
252      }
253  }
254  
255  // MARK: - JS Performance Charts Section
256  
257  struct JSPerformanceChartsSection: View {
258      let analysis: JSAnalysis
259      
260      var body: some View {
261          VStack(alignment: .leading, spacing: 20) {
262              Text("Performance Analysis Charts")
263                  .font(.headline)
264                  .fontWeight(.semibold)
265              
266              HStack(spacing: 20) {
267                  // Execution Time Distribution Chart
268                  VStack(alignment: .leading, spacing: 12) {
269                      Text("Execution Time Distribution")
270                          .font(.subheadline)
271                          .fontWeight(.medium)
272                      
273                      Chart(analysis.executionTimeDistribution) { item in
274                          BarMark(
275                              x: .value("Time Range", item.range),
276                              y: .value("Files", item.count)
277                          )
278                          .foregroundStyle(.red.gradient)
279                          .cornerRadius(4)
280                      }
281                      .frame(height: 180)
282                      .chartXAxis {
283                          AxisMarks { _ in
284                              AxisValueLabel()
285                                  .font(.caption)
286                          }
287                      }
288                  }
289                  .frame(maxWidth: .infinity)
290                  
291                  // File Size vs Execution Time Chart
292                  VStack(alignment: .leading, spacing: 12) {
293                      Text("Size vs Execution Time")
294                          .font(.subheadline)
295                          .fontWeight(.medium)
296                      
297                      Chart(analysis.jsFiles.prefix(15)) { file in
298                          PointMark(
299                              x: .value("File Size (KB)", file.fileSize / 1000),
300                              y: .value("Execution Time (ms)", file.executionTime)
301                          )
302                          .foregroundStyle(file.isRenderBlocking ? .red : .blue)
303                          .symbolSize(file.isRenderBlocking ? 50 : 30)
304                      }
305                      .frame(height: 180)
306                  }
307                  .frame(maxWidth: .infinity)
308              }
309              
310              // Loading Strategy Distribution
311              VStack(alignment: .leading, spacing: 12) {
312                  Text("Loading Strategy Distribution")
313                      .font(.subheadline)
314                      .fontWeight(.medium)
315                  
316                  Chart(analysis.loadingStrategyDistribution) { item in
317                      SectorMark(
318                          angle: .value("Count", item.count),
319                          innerRadius: .ratio(0.5),
320                          angularInset: 2
321                      )
322                      .foregroundStyle(item.strategy.color.gradient)
323                      .cornerRadius(3)
324                  }
325                  .frame(height: 200)
326              }
327          }
328          .padding(20)
329          .background(Color(NSColor.controlBackgroundColor))
330          .cornerRadius(12)
331      }
332  }
333  
334  // MARK: - Critical JS Issues Section
335  
336  struct CriticalJSIssuesSection: View {
337      let issues: [JSIssue]
338      
339      var body: some View {
340          VStack(alignment: .leading, spacing: 16) {
341              HStack {
342                  Image(systemName: "exclamationmark.triangle.fill")
343                      .foregroundColor(.red)
344                  Text("Critical JavaScript Issues")
345                      .font(.headline)
346                      .fontWeight(.semibold)
347                  
348                  Spacer()
349                  
350                  Text("\(issues.count) issues")
351                      .font(.subheadline)
352                      .foregroundColor(.red)
353                      .padding(.horizontal, 12)
354                      .padding(.vertical, 4)
355                      .background(.red.opacity(0.1))
356                      .cornerRadius(8)
357              }
358              
359              LazyVStack(spacing: 8) {
360                  ForEach(issues.prefix(10)) { issue in
361                      CriticalJSIssueRow(issue: issue)
362                  }
363                  
364                  if issues.count > 10 {
365                      Text("... and \(issues.count - 10) more critical issues")
366                          .font(.caption)
367                          .foregroundColor(.secondary)
368                          .padding(.top, 8)
369                  }
370              }
371          }
372          .padding(20)
373          .background(Color(NSColor.controlBackgroundColor))
374          .cornerRadius(12)
375      }
376  }
377  
378  struct CriticalJSIssueRow: View {
379      let issue: JSIssue
380      
381      var body: some View {
382          HStack {
383              Image(systemName: issue.severity.icon)
384                  .foregroundColor(issue.severity.color)
385                  .font(.title2)
386              
387              VStack(alignment: .leading, spacing: 4) {
388                  Text(issue.title)
389                      .font(.subheadline)
390                      .fontWeight(.medium)
391                  
392                  Text(issue.description)
393                      .font(.caption)
394                      .foregroundColor(.secondary)
395                  
396                  if !issue.filename.isEmpty {
397                      Text("File: \(issue.filename)")
398                          .font(.caption)
399                          .foregroundColor(.blue)
400                  }
401              }
402              
403              Spacer()
404              
405              VStack(alignment: .trailing, spacing: 4) {
406                  Text(issue.severity.displayName)
407                      .font(.caption)
408                      .fontWeight(.medium)
409                      .foregroundColor(.white)
410                      .padding(.horizontal, 8)
411                      .padding(.vertical, 4)
412                      .background(issue.severity.color)
413                      .cornerRadius(6)
414                  
415                  if let impact = issue.performanceImpact {
416                      Text("Impact: \(impact)ms")
417                          .font(.caption)
418                          .foregroundColor(.secondary)
419                  }
420              }
421          }
422          .padding(12)
423          .background(.red.opacity(0.05))
424          .cornerRadius(8)
425      }
426  }
427  
428  // MARK: - Large JS Files Section
429  
430  struct LargeJSFilesSection: View {
431      let files: [JSFile]
432      
433      var body: some View {
434          VStack(alignment: .leading, spacing: 16) {
435              HStack {
436                  Image(systemName: "exclamationmark.circle.fill")
437                      .foregroundColor(.orange)
438                  Text("Large JavaScript Files")
439                      .font(.headline)
440                      .fontWeight(.semibold)
441                  
442                  Spacer()
443                  
444                  Text("\(files.count) files")
445                      .font(.subheadline)
446                      .foregroundColor(.orange)
447                      .padding(.horizontal, 12)
448                      .padding(.vertical, 4)
449                      .background(.orange.opacity(0.1))
450                      .cornerRadius(8)
451              }
452              
453              LazyVStack(spacing: 8) {
454                  ForEach(files.prefix(10)) { file in
455                      LargeJSFileRow(file: file)
456                  }
457                  
458                  if files.count > 10 {
459                      Text("... and \(files.count - 10) more large files")
460                          .font(.caption)
461                          .foregroundColor(.secondary)
462                          .padding(.top, 8)
463                  }
464              }
465          }
466          .padding(20)
467          .background(Color(NSColor.controlBackgroundColor))
468          .cornerRadius(12)
469      }
470  }
471  
472  struct LargeJSFileRow: View {
473      let file: JSFile
474      
475      var body: some View {
476          HStack {
477              Image(systemName: "curlybraces")
478                  .foregroundColor(.blue)
479                  .font(.title2)
480              
481              VStack(alignment: .leading, spacing: 4) {
482                  Text(file.filename)
483                      .font(.subheadline)
484                      .fontWeight(.medium)
485                      .lineLimit(1)
486                  
487                  Text(file.url)
488                      .font(.caption)
489                      .foregroundColor(.secondary)
490                      .lineLimit(1)
491                  
492                  HStack {
493                      if file.isRenderBlocking {
494                          Text("Render Blocking")
495                              .font(.caption)
496                              .fontWeight(.medium)
497                              .foregroundColor(.white)
498                              .padding(.horizontal, 6)
499                              .padding(.vertical, 2)
500                              .background(.red)
501                              .cornerRadius(4)
502                      }
503                      
504                      if file.isThirdParty {
505                          Text("Third-party")
506                              .font(.caption)
507                              .fontWeight(.medium)
508                              .foregroundColor(.white)
509                              .padding(.horizontal, 6)
510                              .padding(.vertical, 2)
511                              .background(.purple)
512                              .cornerRadius(4)
513                      }
514                      
515                      if file.isAsync {
516                          Text("Async")
517                              .font(.caption)
518                              .fontWeight(.medium)
519                              .foregroundColor(.white)
520                              .padding(.horizontal, 6)
521                              .padding(.vertical, 2)
522                              .background(.green)
523                              .cornerRadius(4)
524                      }
525                  }
526              }
527              
528              Spacer()
529              
530              VStack(alignment: .trailing, spacing: 4) {
531                  Text(formatFileSize(file.fileSize))
532                      .font(.subheadline)
533                      .fontWeight(.bold)
534                      .foregroundColor(.orange)
535                  
536                  Text("\(Int(file.executionTime))ms")
537                      .font(.caption)
538                      .foregroundColor(.secondary)
539                  
540                  Text(formatMemorySize(file.memoryUsage))
541                      .font(.caption)
542                      .foregroundColor(.secondary)
543              }
544          }
545          .padding(12)
546          .background(.orange.opacity(0.05))
547          .cornerRadius(8)
548      }
549      
550      private func formatFileSize(_ bytes: Int) -> String {
551          let formatter = ByteCountFormatter()
552          formatter.allowedUnits = [.useKB, .useMB]
553          formatter.countStyle = .file
554          return formatter.string(fromByteCount: Int64(bytes))
555      }
556      
557      private func formatMemorySize(_ bytes: Int) -> String {
558          let formatter = ByteCountFormatter()
559          formatter.allowedUnits = [.useKB, .useMB]
560          formatter.countStyle = .memory
561          return formatter.string(fromByteCount: Int64(bytes))
562      }
563  }
564  
565  // MARK: - Render Blocking JS Section
566  
567  struct RenderBlockingJSSection: View {
568      let files: [JSFile]
569      
570      var body: some View {
571          VStack(alignment: .leading, spacing: 16) {
572              HStack {
573                  Image(systemName: "exclamationmark.triangle.fill")
574                      .foregroundColor(.red)
575                  Text("Render Blocking JavaScript")
576                      .font(.headline)
577                      .fontWeight(.semibold)
578                  
579                  Spacer()
580                  
581                  Text("\(files.count) files")
582                      .font(.subheadline)
583                      .foregroundColor(.red)
584                      .padding(.horizontal, 12)
585                      .padding(.vertical, 4)
586                      .background(.red.opacity(0.1))
587                      .cornerRadius(8)
588              }
589              
590              Text("These scripts block the rendering of the page and should be optimized or moved to load asynchronously.")
591                  .font(.caption)
592                  .foregroundColor(.secondary)
593              
594              LazyVStack(spacing: 8) {
595                  ForEach(files.prefix(10)) { file in
596                      RenderBlockingJSRow(file: file)
597                  }
598                  
599                  if files.count > 10 {
600                      Text("... and \(files.count - 10) more render blocking files")
601                          .font(.caption)
602                          .foregroundColor(.secondary)
603                          .padding(.top, 8)
604                  }
605              }
606          }
607          .padding(20)
608          .background(Color(NSColor.controlBackgroundColor))
609          .cornerRadius(12)
610      }
611  }
612  
613  struct RenderBlockingJSRow: View {
614      let file: JSFile
615      
616      var body: some View {
617          HStack {
618              Image(systemName: "exclamationmark.triangle.fill")
619                  .foregroundColor(.red)
620              
621              VStack(alignment: .leading, spacing: 4) {
622                  Text(file.filename)
623                      .font(.subheadline)
624                      .fontWeight(.medium)
625                      .lineLimit(1)
626                  
627                  Text(file.url)
628                      .font(.caption)
629                      .foregroundColor(.secondary)
630                      .lineLimit(1)
631                  
632                  Text("Blocking render for \(Int(file.executionTime))ms")
633                      .font(.caption)
634                      .foregroundColor(.red)
635              }
636              
637              Spacer()
638              
639              VStack(alignment: .trailing, spacing: 4) {
640                  Text("BLOCKING")
641                      .font(.caption)
642                      .fontWeight(.bold)
643                      .foregroundColor(.white)
644                      .padding(.horizontal, 8)
645                      .padding(.vertical, 4)
646                      .background(.red)
647                      .cornerRadius(6)
648                  
649                  Text(formatFileSize(file.fileSize))
650                      .font(.caption)
651                      .foregroundColor(.secondary)
652              }
653          }
654          .padding(12)
655          .background(.red.opacity(0.05))
656          .cornerRadius(8)
657      }
658      
659      private func formatFileSize(_ bytes: Int) -> String {
660          let formatter = ByteCountFormatter()
661          formatter.allowedUnits = [.useKB, .useMB]
662          formatter.countStyle = .file
663          return formatter.string(fromByteCount: Int64(bytes))
664      }
665  }
666  
667  // MARK: - Third-party Scripts Section
668  
669  struct ThirdPartyScriptsSection: View {
670      let scripts: [ThirdPartyScript]
671      
672      var body: some View {
673          VStack(alignment: .leading, spacing: 16) {
674              HStack {
675                  Image(systemName: "globe")
676                      .foregroundColor(.purple)
677                  Text("Third-party Scripts")
678                      .font(.headline)
679                      .fontWeight(.semibold)
680                  
681                  Spacer()
682                  
683                  Text("\(scripts.count) scripts")
684                      .font(.subheadline)
685                      .foregroundColor(.purple)
686                      .padding(.horizontal, 12)
687                      .padding(.vertical, 4)
688                      .background(.purple.opacity(0.1))
689                      .cornerRadius(8)
690              }
691              
692              LazyVStack(spacing: 8) {
693                  ForEach(scripts.prefix(10)) { script in
694                      ThirdPartyScriptRow(script: script)
695                  }
696                  
697                  if scripts.count > 10 {
698                      Text("... and \(scripts.count - 10) more third-party scripts")
699                          .font(.caption)
700                          .foregroundColor(.secondary)
701                          .padding(.top, 8)
702                  }
703              }
704          }
705          .padding(20)
706          .background(Color(NSColor.controlBackgroundColor))
707          .cornerRadius(12)
708      }
709  }
710  
711  struct ThirdPartyScriptRow: View {
712      let script: ThirdPartyScript
713      
714      var body: some View {
715          HStack {
716              VStack(alignment: .leading, spacing: 4) {
717                  Text(script.domain)
718                      .font(.subheadline)
719                      .fontWeight(.medium)
720                  
721                  Text(script.purpose)
722                      .font(.caption)
723                      .foregroundColor(.secondary)
724                  
725                  Text("\(script.fileCount) files")
726                      .font(.caption)
727                      .foregroundColor(.blue)
728              }
729              
730              Spacer()
731              
732              VStack(alignment: .trailing, spacing: 4) {
733                  Text(formatFileSize(script.totalSize))
734                      .font(.caption)
735                      .fontWeight(.medium)
736                      .foregroundColor(.purple)
737                  
738                  Text("\(Int(script.totalExecutionTime))ms")
739                      .font(.caption)
740                      .foregroundColor(.secondary)
741                  
742                  Text(script.impactLevel.rawValue)
743                      .font(.caption)
744                      .fontWeight(.medium)
745                      .foregroundColor(.white)
746                      .padding(.horizontal, 6)
747                      .padding(.vertical, 2)
748                      .background(script.impactLevel.color)
749                      .cornerRadius(4)
750              }
751          }
752          .padding(10)
753          .background(Color(NSColor.textBackgroundColor))
754          .cornerRadius(8)
755      }
756      
757      private func formatFileSize(_ bytes: Int) -> String {
758          let formatter = ByteCountFormatter()
759          formatter.allowedUnits = [.useKB, .useMB]
760          formatter.countStyle = .file
761          return formatter.string(fromByteCount: Int64(bytes))
762      }
763  }
764  
765  // MARK: - Supporting Views
766  
767  struct JSFiltersSection: View {
768      @Binding var selectedFilter: String
769      @Binding var selectedIssue: String
770      
771      var body: some View {
772          VStack(alignment: .leading, spacing: 16) {
773              Text("Filters")
774                  .font(.headline)
775                  .fontWeight(.semibold)
776              
777              HStack(spacing: 20) {
778                  VStack(alignment: .leading, spacing: 8) {
779                      Text("File Type")
780                          .font(.subheadline)
781                          .fontWeight(.medium)
782                      
783                      Picker("Filter", selection: $selectedFilter) {
784                          Text("All Files").tag("All Files")
785                          Text("External Only").tag("External Only")
786                          Text("Internal Only").tag("Internal Only")
787                          Text("Large Files").tag("Large Files")
788                          Text("Render Blocking").tag("Render Blocking")
789                          Text("Third-party").tag("Third-party")
790                      }
791                      .pickerStyle(MenuPickerStyle())
792                  }
793                  
794                  VStack(alignment: .leading, spacing: 8) {
795                      Text("Issues")
796                          .font(.subheadline)
797                          .fontWeight(.medium)
798                      
799                      Picker("Issues", selection: $selectedIssue) {
800                          Text("All Issues").tag("All Issues")
801                          Text("Slow Execution").tag("Slow Execution")
802                          Text("Large Size").tag("Large Size")
803                          Text("Blocking Render").tag("Blocking Render")
804                          Text("Memory Issues").tag("Memory Issues")
805                          Text("No Issues").tag("No Issues")
806                      }
807                      .pickerStyle(MenuPickerStyle())
808                  }
809                  
810                  Spacer()
811              }
812          }
813          .padding(20)
814          .background(Color(NSColor.controlBackgroundColor))
815          .cornerRadius(12)
816      }
817  }
818  
819  struct JSFilesListSection: View {
820      let files: [JSFile]
821      let selectedFilter: String
822      let selectedIssue: String
823      
824      var body: some View {
825          VStack(alignment: .leading, spacing: 16) {
826              HStack {
827                  Text("JavaScript Files")
828                      .font(.headline)
829                      .fontWeight(.semibold)
830                  
831                  Spacer()
832                  
833                  Text("\(files.count) files")
834                      .font(.subheadline)
835                      .foregroundColor(.secondary)
836              }
837              
838              LazyVStack(spacing: 8) {
839                  ForEach(files.prefix(50)) { file in
840                      JSFileRow(file: file)
841                  }
842                  
843                  if files.count > 50 {
844                      Text("... and \(files.count - 50) more files")
845                          .font(.caption)
846                          .foregroundColor(.secondary)
847                          .padding(.top, 8)
848                  }
849              }
850          }
851          .padding(20)
852          .background(Color(NSColor.controlBackgroundColor))
853          .cornerRadius(12)
854      }
855  }
856  
857  struct JSFileRow: View {
858      let file: JSFile
859      
860      var body: some View {
861          HStack {
862              Image(systemName: "curlybraces")
863                  .foregroundColor(.blue)
864                  .font(.title3)
865              
866              VStack(alignment: .leading, spacing: 4) {
867                  Text(file.filename)
868                      .font(.subheadline)
869                      .fontWeight(.medium)
870                      .lineLimit(1)
871                  
872                  Text(file.url)
873                      .font(.caption)
874                      .foregroundColor(.secondary)
875                      .lineLimit(1)
876                  
877                  HStack {
878                      if file.isRenderBlocking {
879                          Text("Blocking")
880                              .font(.caption)
881                              .foregroundColor(.white)
882                              .padding(.horizontal, 4)
883                              .padding(.vertical, 1)
884                              .background(.red)
885                              .cornerRadius(3)
886                      }
887                      
888                      if file.isThirdParty {
889                          Text("3rd Party")
890                              .font(.caption)
891                              .foregroundColor(.white)
892                              .padding(.horizontal, 4)
893                              .padding(.vertical, 1)
894                              .background(.purple)
895                              .cornerRadius(3)
896                      }
897                      
898                      if file.isAsync {
899                          Text("Async")
900                              .font(.caption)
901                              .foregroundColor(.white)
902                              .padding(.horizontal, 4)
903                              .padding(.vertical, 1)
904                              .background(.green)
905                              .cornerRadius(3)
906                      }
907                  }
908              }
909              
910              Spacer()
911              
912              VStack(alignment: .trailing, spacing: 4) {
913                  Text(formatFileSize(file.fileSize))
914                      .font(.caption)
915                      .fontWeight(.medium)
916                      .foregroundColor(file.fileSize > 100000 ? .orange : .secondary)
917                  
918                  Text("\(Int(file.executionTime))ms")
919                      .font(.caption)
920                      .foregroundColor(file.executionTime > 100 ? .red : .secondary)
921                  
922                  Text(formatMemorySize(file.memoryUsage))
923                      .font(.caption)
924                      .foregroundColor(.secondary)
925              }
926          }
927          .padding(10)
928          .background(Color(NSColor.textBackgroundColor))
929          .cornerRadius(8)
930      }
931      
932      private func formatFileSize(_ bytes: Int) -> String {
933          let formatter = ByteCountFormatter()
934          formatter.allowedUnits = [.useKB, .useMB]
935          formatter.countStyle = .file
936          return formatter.string(fromByteCount: Int64(bytes))
937      }
938      
939      private func formatMemorySize(_ bytes: Int) -> String {
940          let formatter = ByteCountFormatter()
941          formatter.allowedUnits = [.useKB, .useMB]
942          formatter.countStyle = .memory
943          return formatter.string(fromByteCount: Int64(bytes))
944      }
945  }
946  
947  struct JSRecommendationsSection: View {
948      let analysis: JSAnalysis
949      
950      var body: some View {
951          VStack(alignment: .leading, spacing: 16) {
952              Text("JavaScript Performance Recommendations")
953                  .font(.headline)
954                  .fontWeight(.semibold)
955              
956              LazyVStack(spacing: 12) {
957                  ForEach(analysis.recommendations, id: \.title) { recommendation in
958                      CommonSEORecommendationCard(recommendation: recommendation)
959                  }
960              }
961          }
962          .padding(20)
963          .background(Color(NSColor.controlBackgroundColor))
964          .cornerRadius(12)
965      }
966  }
967  
968  struct JSEmptyStateView: View {
969      var body: some View {
970          VStack(spacing: 16) {
971              Image(systemName: "curlybraces")
972                  .font(.system(size: 64))
973                  .foregroundColor(.secondary)
974              
975              Text("No JavaScript Analysis Yet")
976                  .font(.title2)
977                  .fontWeight(.semibold)
978              
979              Text("Enter a website URL above to start analyzing JavaScript files for performance optimization and SEO impact.")
980                  .font(.subheadline)
981                  .foregroundColor(.secondary)
982                  .multilineTextAlignment(.center)
983                  .padding(.horizontal, 40)
984          }
985          .frame(maxWidth: .infinity, maxHeight: .infinity)
986      }
987  }
988  
989  #Preview {
990      JSAnalysisView()
991          .environmentObject(SettingsService.shared)
992  }