File size: 5,360 Bytes
68af3c5
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68af3c5
9dfccd9
 
68af3c5
9dfccd9
68af3c5
 
9dfccd9
 
 
 
 
 
 
68af3c5
 
 
 
 
 
 
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68af3c5
 
 
 
 
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useState, useEffect, useRef } from 'react'
import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '@/lib/http'
import { LoadingSkeleton } from '@/components/common/LoadingSkeleton'
import { cn } from '@/lib/utils'

interface HistoryItem {
  id:           string
  query:        string
  answer_brief: string
  created_at:   string
  success:      boolean
  duration_ms:  number
}

interface HistoryResponse {
  items: HistoryItem[]
  total: number
}

async function fetchHistory(page: number): Promise<HistoryResponse> {
  const res = await apiFetch(`/api/workspace/history?page=${page}&limit=20`)
  return res.json()
}

interface Props {
  onReplay?: (query: string) => void
  focusId?:  string
}

export function QueryHistory({ onReplay, focusId }: Props) {
  const [page,     setPage]     = useState(1)
  const [expanded, setExpanded] = useState<string | null>(focusId ?? null)
  const focusRef = useRef<HTMLDivElement>(null)

  const { data, isLoading } = useQuery({
    queryKey:  ['workspace-history', page],
    queryFn:   () => fetchHistory(page),
    staleTime: 60_000,
  })

  // Scroll the focused item into view once data loads
  useEffect(() => {
    if (focusId && focusRef.current) {
      focusRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' })
    }
  }, [focusId, data])

  const totalPages = data ? Math.ceil(data.total / 20) : 1

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <p className="text-sm font-medium text-stone-500">Query History</p>
        {data && (
          <p className="text-xs text-stone-400">{data.total} total queries</p>
        )}
      </div>

      {isLoading ? (
        <div className="rounded-xl border border-surface-subtle p-5">
          <LoadingSkeleton rows={5} />
        </div>
      ) : (data?.items ?? []).length === 0 ? (
        <div className="rounded-xl border border-dashed border-surface-subtle py-16 text-center">
          <p className="text-sm text-stone-400">No queries yet. Ask something to get started.</p>
        </div>
      ) : (
        <div className="flex flex-col gap-2">
          {(data?.items ?? []).map((item) => (
            <div
              key={item.id}
              ref={item.id === focusId ? focusRef : undefined}
              className={cn(
                'rounded-xl border border-surface-subtle transition-colors hover:border-stone-300 dark:hover:border-stone-600',
                item.id === focusId && 'border-brand/50 dark:border-brand/40',
              )}
            >
              <button
                className="flex w-full items-start gap-3 p-4 text-left"
                onClick={() => setExpanded(expanded === item.id ? null : item.id)}
              >
                <span
                  className={cn(
                    'mt-1 h-2 w-2 flex-shrink-0 rounded-full',
                    item.success ? 'bg-green-500' : 'bg-red-400',
                  )}
                  aria-label={item.success ? 'successful' : 'failed'}
                />
                <div className="min-w-0 flex-1">
                  <p className="truncate font-medium">{item.query}</p>
                  <p className="mt-0.5 text-xs text-stone-400">
                    {new Date(item.created_at).toLocaleString()} · {(item.duration_ms / 1000).toFixed(1)}s
                  </p>
                </div>
                <svg
                  className={cn('h-4 w-4 flex-shrink-0 text-stone-400 transition-transform', expanded === item.id && 'rotate-180')}
                  viewBox="0 0 20 20"
                  fill="currentColor"
                >
                  <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
                </svg>
              </button>

              {expanded === item.id && (
                <div className="border-t border-surface-subtle px-4 pb-4 pt-3">
                  <p className="text-sm text-stone-600 dark:text-stone-400">{item.answer_brief}</p>
                  {onReplay && (
                    <button
                      onClick={() => onReplay(item.query)}
                      className="mt-3 text-xs font-medium text-brand hover:underline"
                    >
                      Ask again →
                    </button>
                  )}
                </div>
              )}
            </div>
          ))}
        </div>
      )}

      {/* Pagination */}
      {totalPages > 1 && (
        <div className="flex items-center justify-center gap-2">
          <button
            onClick={() => setPage((p) => Math.max(1, p - 1))}
            disabled={page === 1}
            className="rounded-lg border border-surface-subtle px-3 py-1.5 text-xs font-medium disabled:opacity-40"
          >
            ← Previous
          </button>
          <span className="text-xs text-stone-500">Page {page} of {totalPages}</span>
          <button
            onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
            disabled={page === totalPages}
            className="rounded-lg border border-surface-subtle px-3 py-1.5 text-xs font-medium disabled:opacity-40"
          >
            Next →
          </button>
        </div>
      )}
    </div>
  )
}