RSS Git Download  Clone
Raw Blame History 8kB 174 lines
import { useMemo, useState } from 'react'
import { Box, TextField, Button, InputAdornment } from '@mui/material'
import { Search, Close, MenuBook } from '@mui/icons-material'
import { useI18nStore } from '../stores/i18n.store'
import { useRedisStateStore } from '../stores/redis-state.store'
import P3xrDialog from '../components/P3xrDialog'

interface AiCheatsheetDialogProps {
    open: boolean
    onClose: () => void
    onPick: (prompt: string) => void
}

interface CheatGroup {
    key: string
    name: string
    description?: string
    prompts: string[]
}

export default function AiCheatsheetDialog({ open, onClose, onPick }: AiCheatsheetDialogProps) {
    const strings = useI18nStore(s => s.strings)
    const modules = useRedisStateStore(s => s.modules) || []
    const info = useRedisStateStore(s => s.info)

    const [filter, setFilter] = useState('')

    const moduleNames = useMemo(() => modules.map((m: any) => (m?.name || '').toLowerCase()), [modules])
    const version = useMemo(() => {
        const v = info?.server?.redis_version || ''
        const match = /^(\d+)/.exec(v)
        return match ? parseInt(match[1], 10) : 0
    }, [info])
    const isCluster = info?.server?.redis_mode === 'cluster'

    const visibleGroups = useMemo<CheatGroup[]>(() => {
        const cs = strings?.label?.cheatsheet?.groups
        if (!cs) return []
        const result: CheatGroup[] = []
        const push = (key: string, g: any) => {
            if (!g || !Array.isArray(g.prompts) || g.prompts.length === 0) return
            result.push({ key, name: g.name, description: g.description, prompts: g.prompts })
        }
        push('diagnostics', cs.diagnostics)
        push('keys', cs.keys)
        push('dataTypes', cs.dataTypes)
        if (moduleNames.includes('rejson') || moduleNames.includes('rejson-rl') || moduleNames.includes('json')) push('json', cs.json)
        if (moduleNames.includes('search') || moduleNames.includes('searchlight')) push('search', cs.search)
        if (moduleNames.includes('timeseries')) push('timeseries', cs.timeseries)
        if (moduleNames.includes('bf')) push('bloom', cs.bloom)
        if (version >= 8) {
            push('vectorSet', cs.vectorSet)
            push('redis8', cs.redis8)
        }
        push('scripting', cs.scripting)
        if (isCluster) push('cluster', cs.cluster)
        if (version >= 6) push('acl', cs.acl)
        push('qna', cs.qna)
        push('translate', cs.translate)
        return result
    }, [strings, moduleNames, version, isCluster])

    const filterPrompts = (prompts: string[]) => {
        const q = filter.trim().toLowerCase()
        if (!q) return prompts
        return prompts.filter(p => p.toLowerCase().includes(q))
    }

    const emptyResults = visibleGroups.every(g => filterPrompts(g.prompts).length === 0)

    const cs = strings?.label?.cheatsheet

    if (!open) return null

    return (
        <P3xrDialog open onClose={onClose} title={cs?.title}
            width="720px"
            contentPadding={false}
            actions={
                <>
                    <Box sx={{ flex: '1 1 auto' }} />
                    <Button variant="contained" color="primary" size="small"
                            onClick={() => window.open('https://redis.io/docs/latest/commands/', '_blank')}>
                        <MenuBook fontSize="small" />
                        <span style={{ marginLeft: 3 }}>{cs?.openOfficialDocs}</span>
                    </Button>
                    <Button variant="contained" color="primary" size="small" onClick={onClose}>
                        <Close fontSize="small" />
                        <span style={{ marginLeft: 3 }}>{strings?.intention?.close}</span>
                    </Button>
                </>
            }>

            {/* Sticky header — P3xrDialog has contentPadding={false} so we own all
                internal padding. Sticky sits at the true top of the scroll container
                with its own consistent padding all around. */}
            <Box sx={{
                position: 'sticky',
                top: 0,
                zIndex: 2,
                bgcolor: 'background.paper',
                borderBottom: '1px solid rgba(127,127,127,0.2)',
                px: 2,
                py: 1.5,
            }}>
                {cs?.subtitle && (
                    <Box sx={{ fontSize: 13, opacity: 0.8, lineHeight: 1.4, pb: 0.5 }}>{cs.subtitle}</Box>
                )}
                {cs?.footerHint && (
                    <Box sx={{ fontSize: 13, opacity: 0.8, lineHeight: 1.4, pb: 0.75 }}>{cs.footerHint}</Box>
                )}
                <TextField fullWidth size="small"
                           variant="filled"
                           hiddenLabel
                           placeholder={cs?.searchPlaceholder || ''}
                           value={filter}
                           onChange={e => setFilter(e.target.value)}
                           onKeyDown={e => e.stopPropagation()}
                           sx={{
                               '& .MuiFormHelperText-root': { display: 'none' },
                               '& .MuiFilledInput-root': { mb: 0 },
                           }}
                           InputProps={{
                               startAdornment: (
                                   <InputAdornment position="start"><Search fontSize="small" /></InputAdornment>
                               ),
                           }} />
            </Box>

            <Box sx={{ p: '12px 16px' }}>
                {visibleGroups.map(g => {
                    const prompts = filterPrompts(g.prompts)
                    if (prompts.length === 0) return null
                    return (
                        <Box key={g.key} sx={{ mb: 2.5 }}>
                            <Box sx={{ fontSize: 14, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', opacity: 0.85, mt: 1 }}>
                                {g.name}
                            </Box>
                            {g.description && (
                                <Box sx={{ fontSize: 12, opacity: 0.65, mt: 0.5, mb: 1 }}>{g.description}</Box>
                            )}
                            <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 0.5 }}>
                                {prompts.map((p, i) => (
                                    <Button key={i} variant="outlined" size="small"
                                            onClick={() => { onPick('ai: ' + p); onClose() }}
                                            sx={{
                                                justifyContent: 'flex-start',
                                                textAlign: 'left',
                                                fontFamily: 'Roboto Mono, monospace',
                                                fontSize: 12,
                                                textTransform: 'none',
                                                letterSpacing: 'normal',
                                                lineHeight: 1.4,
                                                minHeight: 32,
                                                py: 0.75,
                                                px: 1.5,
                                                borderRadius: '4px',
                                                whiteSpace: 'normal',
                                            }}>
                                        {p}
                                    </Button>
                                ))}
                            </Box>
                        </Box>
                    )
                })}
                {emptyResults && (
                    <Box sx={{ p: 3, textAlign: 'center', opacity: 0.6, fontSize: 13 }}>{cs?.empty}</Box>
                )}
            </Box>
        </P3xrDialog>
    )
}