@@ -4,12 +4,11 @@ import type React from "react"
4
4
5
5
import { useEffect , useState } from "react"
6
6
import { Button } from "~/components/ui/button"
7
- import { X , ChevronDown , ChevronRight , Shell , CheckCircle2 } from "lucide-react"
7
+ import { X , ChevronDown , ChevronRight , Shell , CheckCircle2 , ArrowLeftRight } from "lucide-react"
8
8
import axios from "axios" ;
9
9
import { Switch } from "~/components/ui/switch" ;
10
10
import { Card , CardContent , CardHeader , CardTitle } from "~/components/ui/card"
11
11
import { Badge } from "~/components/ui/badge"
12
- import Markdown from 'react-markdown'
13
12
14
13
interface FileData {
15
14
name : string
@@ -105,6 +104,7 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
105
104
const [ activeTab , setActiveTab ] = useState ( "diff" )
106
105
const [ isLargeDiff , setIsLargeDiff ] = useState ( false )
107
106
const [ showLargeDiff , setShowLargeDiff ] = useState ( false )
107
+ const [ isSideBySide , setIsSideBySide ] = useState ( false )
108
108
109
109
useEffect ( ( ) => {
110
110
let result
@@ -306,6 +306,187 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
306
306
)
307
307
}
308
308
309
+ const renderSideBySideDiffTree = ( node : DiffNode , level = 0 ) => {
310
+ const indent = level * 20
311
+ const isExpanded = expandedNodes . has ( node . path )
312
+ const hasChildren = node . children && node . children . length > 0
313
+
314
+ // Determine if this is a primitive value or an object/array
315
+ const isPrimitive =
316
+ ( typeof node . value1 !== "object" || node . value1 === null || Array . isArray ( node . value1 ) ) &&
317
+ ( typeof node . value2 !== "object" || node . value2 === null || Array . isArray ( node . value2 ) )
318
+
319
+ const isArray = Array . isArray ( node . value1 ) || Array . isArray ( node . value2 )
320
+
321
+ // Determine if this node or any of its children have changes
322
+ const hasChanges =
323
+ node . type !== "unchanged" ||
324
+ ( node . children &&
325
+ node . children . some (
326
+ ( child ) =>
327
+ child . type !== "unchanged" ||
328
+ ( child . children && child . children . some ( ( grandchild ) => grandchild . type !== "unchanged" ) ) ,
329
+ ) )
330
+
331
+ // Skip rendering unchanged nodes based on settings
332
+ if ( ( showUnchanged || ( level === 1 && ! hasChanges ) ) && node . type === "unchanged" ) {
333
+ return null
334
+ }
335
+
336
+ // Determine what to show on each side
337
+ const showLeft = node . type !== "added"
338
+ const showRight = node . type !== "removed"
339
+
340
+ return (
341
+ < div key = { node . path } className = "relative" >
342
+ { /* Render the node itself */ }
343
+ < div className = "flex items-start" >
344
+ { /* Left side (old) */ }
345
+ < div
346
+ className = { `break-all flex-1 flex items-start rounded-sm ${
347
+ node . type === "removed" ? "bg-red-50" : node . type === "changed" ? "bg-amber-50" : ""
348
+ } `}
349
+ style = { { visibility : showLeft ? "visible" : "hidden" } }
350
+ >
351
+ < div style = { { paddingLeft : `${ indent } px` } } className = "flex items-start w-full" >
352
+ { /* Expand/collapse button for objects/arrays */ }
353
+ { hasChildren && (
354
+ < button onClick = { ( e ) => toggleNode ( node . path , e ) } className = "mr-1 p-1 hover:bg-gray-200 rounded" >
355
+ { isExpanded ? < ChevronDown className = "h-3 w-3" /> : < ChevronRight className = "h-3 w-3" /> }
356
+ </ button >
357
+ ) }
358
+
359
+ { /* Key name */ }
360
+ < div
361
+ className = { `font-mono py-1 pr-2 flex-shrink-0 ${
362
+ node . type === "removed" ? "text-red-800" : node . type === "changed" ? "text-amber-800" : ""
363
+ } `}
364
+ >
365
+ { node . type === "removed" && "- " }
366
+ { node . key }
367
+ { hasChildren && ! isPrimitive && ": {" }
368
+ { ! hasChildren && ! isPrimitive && node . type === "unchanged" && ": {}" }
369
+ </ div >
370
+
371
+ { /* Value for primitive types */ }
372
+ { isPrimitive && (
373
+ < div className = "flex flex-col w-full" >
374
+ { ( node . type === "removed" || node . type === "changed" || node . type === "unchanged" ) && ! isArray && (
375
+ < div
376
+ className = { `${
377
+ node . type === "removed"
378
+ ? "text-red-800"
379
+ : node . type === "changed"
380
+ ? "text-red-800 bg-red-50"
381
+ : ""
382
+ } py-1 px-2 rounded font-mono`}
383
+ >
384
+ { formatValue ( node . value1 ) }
385
+ </ div >
386
+ ) }
387
+ </ div >
388
+ ) }
389
+ </ div >
390
+ </ div >
391
+
392
+ { /* Divider */ }
393
+ < div className = "w-4" > </ div >
394
+
395
+ { /* Right side (new) */ }
396
+ < div
397
+ className = { `break-all flex-1 flex items-start rounded-sm ${
398
+ node . type === "added" ? "bg-green-50" : node . type === "changed" ? "bg-amber-50" : ""
399
+ } `}
400
+ style = { { visibility : showRight ? "visible" : "hidden" } }
401
+ >
402
+ < div style = { { paddingLeft : `${ indent } px` } } className = "flex items-start w-full" >
403
+ { /* Expand/collapse button for objects/arrays */ }
404
+ { hasChildren && (
405
+ < button onClick = { ( e ) => toggleNode ( node . path , e ) } className = "mr-1 p-1 hover:bg-gray-200 rounded" >
406
+ { isExpanded ? < ChevronDown className = "h-3 w-3" /> : < ChevronRight className = "h-3 w-3" /> }
407
+ </ button >
408
+ ) }
409
+
410
+ { /* Key name */ }
411
+ < div
412
+ className = { `font-mono py-1 pr-2 flex-shrink-0 ${
413
+ node . type === "added" ? "text-green-800" : node . type === "changed" ? "text-amber-800" : ""
414
+ } `}
415
+ >
416
+ { node . type === "added" && "+ " }
417
+ { node . key }
418
+ { hasChildren && ! isPrimitive && ": {" }
419
+ { ! hasChildren && ! isPrimitive && node . type === "unchanged" && ": {}" }
420
+ </ div >
421
+
422
+ { /* Value for primitive types */ }
423
+ { isPrimitive && (
424
+ < div className = "flex flex-col w-full" >
425
+ { ( node . type === "added" || node . type === "changed" || node . type === "unchanged" ) && ! isArray && (
426
+ < div
427
+ className = { `${
428
+ node . type === "added"
429
+ ? "text-green-800"
430
+ : node . type === "changed"
431
+ ? "text-green-800 bg-green-50"
432
+ : ""
433
+ } py-1 px-2 rounded font-mono`}
434
+ >
435
+ { formatValue ( node . type === "added" || node . type === "unchanged" ? node . value2 : node . value2 ) }
436
+ </ div >
437
+ ) }
438
+ </ div >
439
+ ) }
440
+ </ div >
441
+ </ div >
442
+ </ div >
443
+
444
+ { /* Render children if expanded */ }
445
+ { hasChildren && isExpanded && (
446
+ < div >
447
+ { node . children ! . map ( ( child , index ) =>
448
+ renderSideBySideDiffTree ( child , level + 1 ) ,
449
+ ) }
450
+ { ! isPrimitive && (
451
+ < div className = "flex" >
452
+ { /* Left side closing brace */ }
453
+ < div className = "flex-1" >
454
+ { showLeft && (
455
+ < div
456
+ className = { `font-mono py-1 ${
457
+ node . type === "removed" ? "text-red-800" : node . type === "changed" ? "text-amber-800" : ""
458
+ } `}
459
+ style = { { paddingLeft : `${ indent } px` } }
460
+ >
461
+ { node . type === "removed" && "- " }
462
+ { "}" }
463
+ </ div >
464
+ ) }
465
+ </ div >
466
+ { /* Divider */ }
467
+ < div className = "w-4" > </ div >
468
+ { /* Right side closing brace */ }
469
+ < div className = "flex-1" >
470
+ { showRight && (
471
+ < div
472
+ className = { `font-mono py-1 ${
473
+ node . type === "added" ? "text-green-800" : node . type === "changed" ? "text-amber-800" : ""
474
+ } `}
475
+ style = { { paddingLeft : `${ indent } px` } }
476
+ >
477
+ { node . type === "added" && "+ " }
478
+ { "}" }
479
+ </ div >
480
+ ) }
481
+ </ div >
482
+ </ div >
483
+ ) }
484
+ </ div >
485
+ ) }
486
+ </ div >
487
+ )
488
+ }
489
+
309
490
310
491
const renderApiChangesView = ( ) => {
311
492
if ( ! apiChanges ) {
@@ -742,9 +923,25 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
742
923
</ div >
743
924
) : ! ! diffTree ? (
744
925
< div className = "text-sm" >
745
- { diffTree . children ?. map ( ( child , _ ) =>
746
- renderDiffTree ( child , 1 ) ,
747
- ) }
926
+ < div className = "text-sm" >
927
+ { /* Render header for side-by-side view */ }
928
+ { isSideBySide && (
929
+ < div className = "flex mb-2 font-medium text-sm" >
930
+ < div className = "flex-1 px-2 py-1 bg-gray-100 rounded-t" > Old Version</ div >
931
+ < div className = "w-4" > </ div >
932
+ < div className = "flex-1 px-2 py-1 bg-gray-100 rounded-t" > New Version</ div >
933
+ </ div >
934
+ ) }
935
+
936
+ { /* Render the appropriate diff view based on the toggle */ }
937
+ { isSideBySide
938
+ ? diffTree . children ?. map ( ( child , index ) =>
939
+ renderSideBySideDiffTree ( child , 1 ) ,
940
+ )
941
+ : diffTree . children ?. map ( ( child , index ) =>
942
+ renderDiffTree ( child , 1 ) ,
943
+ ) }
944
+ </ div >
748
945
</ div >
749
946
) : (
750
947
< div className = "flex items-center justify-center h-full" >
@@ -861,19 +1058,34 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
861
1058
) }
862
1059
</ div >
863
1060
< div className = "p-4 border-t flex justify-between items-center" >
864
- { activeTab === "diff" && (
1061
+
865
1062
< div className = "flex items-center space-x-2" >
866
- < Switch
867
- checked = { showUnchanged }
868
- onCheckedChange = { ( e ) => setShowUnchanged ( e ) }
869
- id = "hide-unchanged"
870
- />
871
- < label htmlFor = "hide-unchanged" className = "text-sm font-medium cursor-pointer" >
872
- Hide unchanged nodes
873
- </ label >
874
- </ div > ) }
875
- { ( activeTab === "metrics" || activeTab === "api" || activeTab === "ai" ) && < div > </ div > }
876
- < Button onClick = { onClose } > Close</ Button >
1063
+ { activeTab === "diff" && (
1064
+ < >
1065
+ < Switch
1066
+ checked = { showUnchanged }
1067
+ onCheckedChange = { ( e ) => setShowUnchanged ( e ) }
1068
+ id = "hide-unchanged"
1069
+ />
1070
+ < label htmlFor = "hide-unchanged" className = "text-sm font-medium cursor-pointer" >
1071
+ Hide unchanged nodes
1072
+ </ label >
1073
+ </ >
1074
+ ) }
1075
+ </ div >
1076
+
1077
+ { /* View toggle button */ }
1078
+ < div className = "flex justify-end mb-4 gap-2" >
1079
+ { ( activeTab === "diff" ) && < Button
1080
+ onClick = { ( ) => setIsSideBySide ( ! isSideBySide ) }
1081
+ >
1082
+ < ArrowLeftRight className = "h-4 w-4" />
1083
+ { isSideBySide ? "Unified View" : "Side-by-Side View" }
1084
+ </ Button >
1085
+ }
1086
+
1087
+ < Button onClick = { onClose } > Close</ Button >
1088
+ </ div >
877
1089
</ div >
878
1090
</ div >
879
1091
</ div >
0 commit comments