SearchInputBox.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { useRef, useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { EditorState, Compartment } from '@codemirror/state';
  4. import { EditorView, keymap, placeholder } from '@codemirror/view';
  5. import { insertTab } from '@codemirror/commands';
  6. import { indentUnit } from '@codemirror/language';
  7. import { minimalSetup } from 'codemirror';
  8. import { javascript } from '@codemirror/lang-javascript';
  9. import { linter, Diagnostic } from '@codemirror/lint';
  10. import { CollectionFullObject } from '@server/types';
  11. import { CollectionService } from '@/http';
  12. import { DataTypeStringEnum } from '@/consts';
  13. import { SearchSingleParams } from '../../types';
  14. import { getQueryStyles } from './Styles';
  15. import { useTheme } from '@mui/material';
  16. import { githubLight } from '@ddietr/codemirror-themes/github-light';
  17. import { githubDark } from '@ddietr/codemirror-themes/github-dark';
  18. import { Validator } from './utils';
  19. export type SearchInputBoxProps = {
  20. onChange: (anns_field: string, value: string) => void;
  21. searchParams: SearchSingleParams;
  22. collection: CollectionFullObject;
  23. type?: 'vector' | 'text';
  24. };
  25. let queryTimeout: NodeJS.Timeout;
  26. export default function SearchInputBox(props: SearchInputBoxProps) {
  27. const theme = useTheme();
  28. const { t: searchTrans } = useTranslation('search');
  29. // props
  30. const { searchParams, onChange, collection, type } = props;
  31. const { field, data } = searchParams;
  32. // classes
  33. const classes = getQueryStyles();
  34. // refs
  35. const editorEl = useRef<HTMLDivElement>(null);
  36. const editor = useRef<EditorView>();
  37. const onChangeRef = useRef(onChange);
  38. const dataRef = useRef(data);
  39. const fieldRef = useRef(field);
  40. const searchParamsRef = useRef(searchParams);
  41. const themeCompartment = new Compartment();
  42. // get validator
  43. const validator = Validator[field.data_type as keyof typeof Validator];
  44. useEffect(() => {
  45. // update dataRef and onChangeRef when data changes
  46. dataRef.current = data;
  47. onChangeRef.current = onChange;
  48. fieldRef.current = field;
  49. searchParamsRef.current = searchParams;
  50. }, [JSON.stringify(searchParams), onChange]);
  51. const getVectorById = (text: string) => {
  52. if (queryTimeout) {
  53. clearTimeout(queryTimeout);
  54. }
  55. // only search for text that doesn't have space, comma, or brackets or curly brackets
  56. if (!text.trim().match(/[\s,{}]/)) {
  57. const isVarChar =
  58. collection.schema.primaryField.data_type === DataTypeStringEnum.VarChar;
  59. if (!isVarChar && isNaN(Number(text))) {
  60. return;
  61. }
  62. queryTimeout = setTimeout(() => {
  63. try {
  64. CollectionService.queryData(collection.collection_name, {
  65. expr: isVarChar
  66. ? `${collection.schema.primaryField.name} == '${text}'`
  67. : `${collection.schema.primaryField.name} == ${text}`,
  68. output_fields: [searchParamsRef.current.anns_field],
  69. })
  70. .then(res => {
  71. if (res.data && res.data.length === 1) {
  72. onChangeRef.current(
  73. searchParamsRef.current.anns_field,
  74. JSON.stringify(
  75. res.data[0][searchParamsRef.current.anns_field]
  76. )
  77. );
  78. }
  79. })
  80. .catch(e => {
  81. console.log(0, e);
  82. });
  83. } catch (e) {}
  84. }, 300);
  85. }
  86. };
  87. // create editor
  88. useEffect(() => {
  89. if (!editor.current) {
  90. // update outside data timeout handler
  91. let updateTimeout: NodeJS.Timeout;
  92. let extensions = [
  93. minimalSetup,
  94. placeholder(
  95. searchTrans(
  96. type === 'text' ? 'textPlaceHolder' : 'inputVectorPlaceHolder'
  97. )
  98. ),
  99. keymap.of([{ key: 'Tab', run: insertTab }]), // fix tab behaviour
  100. indentUnit.of(' '), // fix tab indentation
  101. EditorView.theme({
  102. '&.cm-editor': {
  103. '&.cm-focused': {
  104. outline: 'none',
  105. },
  106. },
  107. '.cm-content': {
  108. fontSize: '12px',
  109. fontFamily: 'IBM Plex Mono, monospace',
  110. minHeight: '124px',
  111. },
  112. '.cm-gutters': {
  113. display: 'none',
  114. },
  115. }),
  116. EditorView.lineWrapping,
  117. EditorView.updateListener.of(update => {
  118. if (update.docChanged) {
  119. if (queryTimeout || updateTimeout) {
  120. clearTimeout(queryTimeout);
  121. clearTimeout(updateTimeout);
  122. }
  123. updateTimeout = setTimeout(() => {
  124. // get text
  125. const text = update.state.doc.toString();
  126. // validate text
  127. const { valid } = validator(text, fieldRef.current);
  128. // if valid, update search params
  129. if (valid || text === '' || type === 'text') {
  130. onChangeRef.current(searchParams.anns_field, text);
  131. } else {
  132. getVectorById(text);
  133. }
  134. }, 500);
  135. }
  136. if (update.focusChanged) {
  137. editorEl.current?.classList.toggle('focused', update.view.hasFocus);
  138. }
  139. }),
  140. ];
  141. if (type === 'vector') {
  142. extensions = [
  143. ...extensions,
  144. javascript(),
  145. linter(view => {
  146. const text = view.state.doc.toString();
  147. // ignore empty text
  148. if (!text) return [];
  149. // validate
  150. const { valid, message } = validator(text, field);
  151. // if invalid, draw a red line
  152. if (!valid) {
  153. let diagnostics: Diagnostic[] = [];
  154. diagnostics.push({
  155. from: 0,
  156. to: view.state.doc.line(view.state.doc.lines).to,
  157. severity: 'error',
  158. message: message,
  159. actions: [
  160. {
  161. name: 'Remove',
  162. apply(view, from, to) {
  163. view.dispatch({ changes: { from, to } });
  164. },
  165. },
  166. ],
  167. });
  168. return diagnostics;
  169. } else {
  170. // onChangeRef.current(searchParams.anns_field, value);
  171. return [];
  172. }
  173. }),
  174. ];
  175. }
  176. // create editor
  177. const startState = EditorState.create({
  178. doc: data,
  179. extensions,
  180. });
  181. // create editor view
  182. const view = new EditorView({
  183. state: startState,
  184. parent: editorEl.current!,
  185. });
  186. // set editor ref
  187. editor.current = view;
  188. } else {
  189. if (editor.current.state.doc.toString() !== data) {
  190. editor.current.dispatch({
  191. changes: {
  192. from: 0,
  193. to: editor.current.state.doc.length,
  194. insert: data,
  195. },
  196. });
  197. }
  198. }
  199. return () => {};
  200. }, [JSON.stringify({ field, data })]);
  201. useEffect(() => {
  202. // dispatch dark mode change to editor
  203. if (editor.current) {
  204. editor.current.dispatch({
  205. effects: themeCompartment.reconfigure(
  206. themeCompartment.of(
  207. theme.palette.mode === 'light' ? githubLight : githubDark
  208. )
  209. ),
  210. });
  211. }
  212. }, [theme.palette.mode]);
  213. return <div className={classes.searchInputBox} ref={editorEl}></div>;
  214. }