File size: 7,601 Bytes
1c04688
 
 
 
2f4e514
 
 
8def8f4
1c04688
 
8de92d1
 
 
1c04688
 
 
 
 
 
8def8f4
1c04688
 
 
 
 
 
8def8f4
1c04688
2f4e514
940813c
2f4e514
484a0e5
 
2f4e514
 
 
940813c
 
 
 
2f4e514
 
940813c
 
 
 
 
2f4e514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484a0e5
2f4e514
 
 
 
 
 
 
 
 
 
 
 
484a0e5
 
 
 
 
 
 
 
 
 
 
 
 
2f4e514
 
 
 
1c04688
2f4e514
8def8f4
2f4e514
 
940813c
 
 
8def8f4
2f4e514
 
 
8def8f4
2f4e514
8def8f4
2f4e514
 
8def8f4
 
 
 
 
 
 
2f4e514
8def8f4
 
 
 
 
 
1c04688
2f4e514
1c04688
2f4e514
 
484a0e5
2f4e514
 
 
 
 
484a0e5
 
 
 
 
 
 
2f4e514
8def8f4
2f4e514
8def8f4
 
2f4e514
 
8def8f4
 
940813c
8def8f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f4e514
 
8def8f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f4e514
 
 
 
 
8def8f4
2f4e514
 
 
 
1c04688
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"use client";

import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { useState, useEffect, useRef } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

interface Run {
  start_article: string;
  destination_article: string;
  steps: string[];
}

interface RunsListProps {
  runs: Run[];
  onSelectRun: (runId: number) => void;
  selectedRunId: number | null;
  onTryRun?: (startArticle: string, destinationArticle: string) => void;
}

export default function RunsList({
  runs,
  onSelectRun,
  selectedRunId,
  onTryRun,
}: RunsListProps) {
  const [isPlaying, setIsPlaying] = useState(true);
  const [filter, setFilter] = useState("");
  const timerRef = useRef<NodeJS.Timeout | null>(null);
  const listContainerRef = useRef<HTMLDivElement>(null);
  const runItemsRef = useRef<Map<number, HTMLDivElement>>(new Map());

  // Filter runs based on start and end filters
  const filteredRuns = runs.filter((run) => {
    const matches = filter === "" || 
      run.start_article.toLowerCase().includes(filter.toLowerCase()) ||
      run.destination_article.toLowerCase().includes(filter.toLowerCase());
    return matches;
  });

  const _onSelectRun = (runId: number) => {
    onSelectRun(runId);
    setIsPlaying(false);
  };

  // Auto-play functionality
  useEffect(() => {
    if (isPlaying) {
      timerRef.current = setInterval(() => {
        if (filteredRuns.length === 0) return;
        
        const nextIndex = selectedRunId === null 
          ? 0 
          : (selectedRunId + 1) % filteredRuns.length;
        
        const originalIndex = runs.findIndex(
          run => run === filteredRuns[nextIndex]
        );
        
        onSelectRun(originalIndex);
      }, 1500);
    } else if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }

    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, [isPlaying, selectedRunId, filteredRuns, runs, onSelectRun]);

  // Scroll selected run into view when it changes
  useEffect(() => {
    if (selectedRunId !== null && isPlaying) {
      const selectedElement = runItemsRef.current.get(selectedRunId);
      if (selectedElement && listContainerRef.current) {
        selectedElement.scrollIntoView({
          behavior: 'smooth',
          block: 'nearest'
        });
      }
    }
  }, [selectedRunId, isPlaying]);

  const togglePlayPause = () => {
    setIsPlaying(prev => !prev);
  };

  return (
    <div className="h-full w-full flex flex-col">
      <div className="space-y-2 mb-4">
        <div className="flex gap-2 items-center">
          <Input
            placeholder="Filter by article"
            value={filter}
            onChange={(e) => setFilter(e.target.value)}
            className="h-9"
          />
          <Button 
            size="sm" 
            variant={isPlaying ? "secondary" : "outline"} 
            onClick={togglePlayPause}
            className="flex-shrink-0 h-9 px-3 gap-1"
          >
            {isPlaying ? (
              <>
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="6" y="4" width="4" height="16" />
                  <rect x="14" y="4" width="4" height="16" />
                </svg>
                Pause
              </>
            ) : (
              <>
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <polygon points="5 3 19 12 5 21 5 3" />
                </svg>
                Play
              </>
            )}
          </Button>
        </div>
      </div>

      <div ref={listContainerRef} className="flex-1 overflow-y-auto overflow-x-hidden space-y-3 pr-1">
        {filteredRuns.map((run) => {
          const originalIndex = runs.indexOf(run);
          return (
            <Card
              key={originalIndex}
              ref={(el) => {
                if (el) {
                  runItemsRef.current.set(originalIndex, el);
                } else {
                  runItemsRef.current.delete(originalIndex);
                }
              }}
              className={cn(
                "p-0 cursor-pointer transition-all border overflow-hidden",
                selectedRunId === originalIndex
                  ? "bg-primary/5 border-primary/50 shadow-md"
                  : "hover:bg-muted/50 border-border"
              )}
            >
              <div 
                className="p-3 flex flex-col gap-2"
                onClick={() => _onSelectRun(originalIndex)}
              >
                <div className="flex items-start justify-between">
                  <div className="space-y-1">
                    <div className="font-medium flex items-center flex-wrap gap-1">
                      <span className="text-primary">{run.start_article}</span>
                      <svg
                        xmlns="http://www.w3.org/2000/svg"
                        width="14"
                        height="14"
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        className="text-muted-foreground"
                      >
                        <path d="M5 12h14" />
                        <path d="m12 5 7 7-7 7" />
                      </svg>
                      <span className="text-primary">{run.destination_article}</span>
                    </div>
                    <div className="flex items-center gap-2">
                      <Badge variant="outline" className="text-xs px-2 py-0 h-5">
                        {run.steps.length} {run.steps.length === 1 ? 'hop' : 'hops'}
                      </Badge>
                      {selectedRunId === originalIndex && (
                        <div className="flex items-center gap-1 text-xs text-primary">
                          <div
                            className="h-2 w-2 rounded-full bg-primary animate-pulse"
                            aria-hidden="true"
                          />
                          <span>Active</span>
                        </div>
                      )}
                    </div>
                  </div>
                </div>
              </div>
              
              {onTryRun && selectedRunId === originalIndex && (
                <div className="border-t px-3 py-2 bg-muted/30 flex justify-end">
                  <Button 
                    size="sm" 
                    variant="outline"
                    className="h-7 text-xs"
                    onClick={(e) => {
                      e.stopPropagation();
                      onTryRun(run.start_article, run.destination_article);
                    }}
                  >
                    Try this path
                  </Button>
                </div>
              )}
            </Card>
          );
        })}

        {filteredRuns.length === 0 && (
          <div className="flex items-center justify-center h-32 text-muted-foreground">
            No runs available
          </div>
        )}
      </div>
    </div>
  );
}