1import { memo, useEffect, useMemo, useState, Fragment } from 'react'; 2 3import jsdocJson from '../../../../../doc.json'; 4import { Textbox } from '@src/repl/components/panel/SettingsTab'; 5import { settingsMap, useSettings } from '@src/settings.mjs'; 6import { referenceFunctions } from '@src/jev/reference.mjs'; 7 8// jevstrudel: the list is jev/reference.mjs's, which the MCP's api_reference reads too 9const availableFunctions = referenceFunctions(jsdocJson.docs); 10 11const tagCounts = {}; 12const ignoredTags = ['supradough', 'superdirt']; 13// const tagOptions = { all: `all (${availableFunctions.length})` }; 14const tagOptions = { all: `all` }; 15for (const doc of availableFunctions) { 16 (doc.tags || ['untagged']).forEach((t) => { 17 if (typeof t === 'string' && t && !ignoredTags.includes(t)) { 18 tagCounts[t] = (tagCounts[t] || 0) + 1; 19 //tagOptions[t] = `${t} (${tagCounts[t]})`; 20 tagOptions[t] = t; 21 } 22 }); 23} 24 25const getInnerText = (html) => { 26 var div = document.createElement('div'); 27 div.innerHTML = html; 28 return div.textContent || div.innerText || ''; 29}; 30 31export const Reference = memo(function Reference() { 32 const [search, setSearch] = useState(''); 33 const [selectedFunction, setSelectedFunction] = useState(null); 34 const { referenceTag } = useSettings(); 35 36 const toggleTag = (tag) => { 37 if (referenceTag === tag) { 38 setReferenceTag('all'); 39 } else { 40 setReferenceTag(tag); 41 } 42 }; 43 44 const searchVisibleFunctions = useMemo(() => { 45 return availableFunctions.filter((entry) => { 46 if (referenceTag && referenceTag !== 'all') { 47 if (!(entry.tags || ['untagged']).includes(referenceTag)) { 48 return false; 49 } 50 } 51 if (!search) { 52 return true; 53 } 54 const lowerCaseSearch = search.toLowerCase(); 55 return ( 56 (entry.allNames || entry.name).toLowerCase().includes(lowerCaseSearch) || 57 (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) 58 ); 59 }); 60 }, [search, referenceTag]); 61 62 const detailVisibleFunctions = useMemo(() => { 63 return searchVisibleFunctions.filter((x) => { 64 if (referenceTag === null || referenceTag === 'all') { 65 if (search) { 66 return true; 67 } 68 return x.name === selectedFunction; 69 } else { 70 return true; 71 } 72 }); 73 }, [searchVisibleFunctions, selectedFunction, referenceTag]); 74 75 const onSearchTagFilterClick = () => { 76 setReferenceTag('all'); 77 setSelectedFunction(null); 78 }; 79 80 useEffect(() => { 81 if (selectedFunction) { 82 const el = document.getElementById(`doc-${selectedFunction}`); 83 const container = document.getElementById('reference-container'); 84 container.scrollTo(0, el.offsetTop); 85 } 86 }, [selectedFunction]); 87 88 let setReferenceTag = (value) => settingsMap.setKey('referenceTag', value); 89 90 return ( 91 <div className="flex h-full w-full overflow-hidden"> 92 <div className="h-full text-foreground flex flex-col w-1/3 border-r border-muted"> 93 {/* bg-background */} 94 <div className="w-full flex"> 95 <Textbox 96 className="w-full border-0 border-b border-muted" 97 placeholder="Search..." 98 value={search} 99 onChange={(e) => { 100 setSelectedFunction(null); 101 setReferenceTag('all'); 102 setSearch(e); 103 }} 104 /> 105 </div> 106 107 {/* <div className="flex shrink-0 flex-wrap w-full overflow-auto border-y border-muted"> 108 <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}></ButtonGroup> 109 </div> */} 110 {referenceTag && referenceTag !== 'all' && ( 111 <div className="w-72"> 112 <span 113 className="text-foreground border border-muted border-t-0 border-l-0 px-1 py-0.5 my-2 cursor-pointer font-sans" 114 onClick={onSearchTagFilterClick} 115 > 116 {referenceTag} 117 </span> 118 </div> 119 )} 120 <div className="h-full p-2 overflow-y-auto bg-opacity-50"> 121 {searchVisibleFunctions.map((entry, i) => ( 122 <Fragment key={`entry-${entry.name}`}> 123 <a 124 className={ 125 'whitespace-nowrap cursor-pointer hover:opacity-50 text-ellipsis block' + 126 (entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '') 127 } 128 onClick={() => { 129 if (entry.name === selectedFunction) { 130 setSelectedFunction(null); 131 } else { 132 setSelectedFunction(entry.name); 133 } 134 }} 135 > 136 {entry.name} {entry.synonyms && <small className="opacity-50">{entry.synonyms?.join(', ')}</small>} 137 </a>{' '} 138 </Fragment> 139 ))} 140 </div> 141 </div> 142 <div 143 className="w-2/3 break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative" 144 id="reference-container" 145 > 146 <div className="prose dark:prose-invert min-w-full px-1 text-sm"> 147 <h2>API Reference</h2> 148 <p className="font-sans text-md"> 149 This is the long list of functions you can use. Remember that you don't need to remember all of those and 150 that you can already make music with a small set of functions! 151 </p> 152 <div> 153 {/* <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}/>*/} 154 {Object.entries(tagCounts) 155 .sort(([a], [b]) => a.localeCompare(b)) 156 .map(([t, count]) => ( 157 <span key={t}> 158 <a 159 className={[ 160 'select-none text-white border border-muted px-1 py-0.5 my-2 cursor-pointer text-sm/8 no-underline font-sans', 161 `${referenceTag === t ? 'bg-muted text-foreground' : ''}`, 162 ].join(' ')} 163 onClick={() => toggleTag(t)} 164 > 165 {t} ({count}) 166 </a>{' '} 167 </span> 168 ))} 169 </div> 170 {detailVisibleFunctions.map((entry, i) => ( 171 <section key={i} className="font-sans"> 172 <div className="flex flex-row items-center mt-8 justify-between"> 173 <h3 className="font-mono my-0 pt-4" id={`doc-${entry.name}`}> 174 {entry.name} 175 </h3> 176 {entry.tags && ( 177 <span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5"> 178 {entry.tags.filter((t) => !ignoredTags.includes(t)).join(', ')} 179 </span> 180 )} 181 </div> 182 {!!entry.synonyms_text && ( 183 <p> 184 Synonyms: <code>{entry.synonyms_text}</code> 185 </p> 186 )} 187 188 <p 189 dangerouslySetInnerHTML={{ 190 // If we have an external url make sure it opens in another tab to not interrupt the player 191 __html: entry.description.replaceAll(/<a href="https:\/\//g, `<a target="_blank" href="https://`), 192 }} 193 ></p> 194 <ul> 195 {entry.params?.map(({ name, type, description }, i) => ( 196 <li key={i}> 197 {name} : {type?.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''} 198 </li> 199 ))} 200 </ul> 201 {entry.examples?.map((example, j) => ( 202 <pre className="bg-background border border-muted" key={j}> 203 {example} 204 </pre> 205 ))} 206 </section> 207 )) || <p className="font-sans">Search or select a tag to get started.</p>} 208 {detailVisibleFunctions.length > 0 && <div className="h-screen" />} 209 </div> 210 </div> 211 </div> 212 ); 213});