File size: 2,451 Bytes
212c959
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { memo } from 'react'
import { Trash2 } from 'lucide-react'

const GENERIC_TITLES = new Set(['new', 'chat', 'conversation', 'session', 'untitled'])
const FILLER_WORDS = new Set([
  'a',
  'an',
  'and',
  'are',
  'for',
  'from',
  'have',
  'how',
  'i',
  'in',
  'is',
  'me',
  'my',
  'of',
  'on',
  'or',
  'please',
  'the',
  'to',
  'we',
  'with',
  'you',
  'your',
])

function tokenize(text) {
  return String(text || '')
    .replace(/(You|Assistant):/gi, ' ')
    .replace(/[^a-z0-9\s]/gi, ' ')
    .split(/\s+/)
    .filter(Boolean)
}

function toTitleCase(word) {
  return word.charAt(0).toUpperCase() + word.slice(1)
}

function buildOverview(session) {
  const sources = [session.title, session.last_message_preview]
  const originalWords = tokenize(sources.join(' '))
  const preferredWords = originalWords.filter((word) => {
    const lower = word.toLowerCase()
    return !GENERIC_TITLES.has(lower) && !FILLER_WORDS.has(lower)
  })

  const chosenWords = (preferredWords.length ? preferredWords : originalWords).slice(0, 3)
  if (!chosenWords.length) return 'New Conversation'

  return chosenWords.map((word) => toTitleCase(word.toLowerCase())).join(' ')
}

function SessionItem({ session, active, onSelect, onDelete }) {
  const overview = buildOverview(session)

  return (
    <div
      className={`group w-full rounded-lg px-3 py-2.5 text-left text-sm transition ${
        active
          ? 'bg-secondary text-foreground'
          : 'bg-transparent text-muted-foreground hover:bg-secondary hover:text-foreground'
      }`}
    >
      <div className="flex items-center gap-2">
        <button
          type="button"
          onClick={() => onSelect(session)}
          title={session.title || overview}
          className="min-w-0 flex-1 text-left"
          aria-pressed={active}
        >
          <span className="block truncate">{overview}</span>
        </button>
        <button
          type="button"
          onClick={() => onDelete?.(session)}
          className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground opacity-100 transition hover:bg-danger/10 hover:text-danger sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100"
          aria-label={`Delete ${overview}`}
          title="Delete chat"
        >
          <Trash2 className="h-3.5 w-3.5" />
        </button>
      </div>
    </div>
  )
}

export default memo(SessionItem)