| 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) |
|
|