jevstrudel.git / website / src / repl / components / panel / Reference.jsx
Reference.jsx213 lines · 7.8 KB · raw
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}&nbsp;({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});