VectorInputBox.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import { useRef, useEffect, useState } from 'react';
  2. import { EditorState, Compartment } from '@codemirror/state';
  3. import { EditorView, keymap } from '@codemirror/view';
  4. import { insertTab } from '@codemirror/commands';
  5. import { indentUnit } from '@codemirror/language';
  6. import { minimalSetup } from 'codemirror';
  7. import { javascript } from '@codemirror/lang-javascript';
  8. import { linter, Diagnostic } from '@codemirror/lint';
  9. import { FieldObject } from '@server/types';
  10. import { DataTypeStringEnum } from '@/consts';
  11. import { SearchSingleParams } from '../../types';
  12. import { isSparseVector, transformObjStrToJSONStr } from '@/utils';
  13. import { getQueryStyles } from './Styles';
  14. import { useTheme } from '@mui/material';
  15. import { githubLight } from '@ddietr/codemirror-themes/github-light';
  16. import { githubDark } from '@ddietr/codemirror-themes/github-dark';
  17. const floatVectorValidator = (text: string, field: FieldObject) => {
  18. try {
  19. const value = JSON.parse(text);
  20. const dim = field.dimension;
  21. if (!Array.isArray(value)) {
  22. return {
  23. valid: false,
  24. message: `Not an array`,
  25. };
  26. }
  27. if (Array.isArray(value) && value.length !== dim) {
  28. return {
  29. valid: false,
  30. value: undefined,
  31. message: `Dimension ${value.length} is not equal to ${dim} `,
  32. };
  33. }
  34. return { valid: true, message: ``, value: value };
  35. } catch (e: any) {
  36. return {
  37. valid: false,
  38. message: `Wrong Float Vector format, it should be an array of ${field.dimension} numbers`,
  39. };
  40. }
  41. };
  42. const binaryVectorValidator = (text: string, field: FieldObject) => {
  43. try {
  44. const value = JSON.parse(text);
  45. const dim = field.dimension;
  46. if (!Array.isArray(value)) {
  47. return {
  48. valid: false,
  49. message: `Not an array`,
  50. };
  51. }
  52. if (Array.isArray(value) && value.length !== dim / 8) {
  53. return {
  54. valid: false,
  55. value: undefined,
  56. message: `Dimension ${value.length} is not equal to ${dim / 8} `,
  57. };
  58. }
  59. return { valid: true, message: ``, value: value };
  60. } catch (e: any) {
  61. return {
  62. valid: false,
  63. message: `Wrong Binary Vector format, it should be an array of ${
  64. field.dimension / 8
  65. } numbers`,
  66. };
  67. }
  68. };
  69. const sparseVectorValidator = (text: string, field: FieldObject) => {
  70. if (!isSparseVector(text)) {
  71. return {
  72. valid: false,
  73. value: undefined,
  74. message: `Incorrect Sparse Vector format, it should be like {1: 0.1, 3: 0.2}`,
  75. };
  76. }
  77. try {
  78. JSON.parse(transformObjStrToJSONStr(text));
  79. return {
  80. valid: true,
  81. message: ``,
  82. };
  83. } catch (e: any) {
  84. return {
  85. valid: false,
  86. message: `Wrong Sparse Vector format`,
  87. };
  88. }
  89. };
  90. const Validator = {
  91. [DataTypeStringEnum.FloatVector]: floatVectorValidator,
  92. [DataTypeStringEnum.BinaryVector]: binaryVectorValidator,
  93. [DataTypeStringEnum.Float16Vector]: floatVectorValidator,
  94. [DataTypeStringEnum.BFloat16Vector]: floatVectorValidator,
  95. [DataTypeStringEnum.SparseFloatVector]: sparseVectorValidator,
  96. };
  97. export type VectorInputBoxProps = {
  98. onChange: (anns_field: string, value: string) => void;
  99. searchParams: SearchSingleParams;
  100. };
  101. export default function VectorInputBox(props: VectorInputBoxProps) {
  102. const theme = useTheme();
  103. // props
  104. const { searchParams, onChange } = props;
  105. const { field, data } = searchParams;
  106. // UI states
  107. const [isFocused, setIsFocused] = useState(false);
  108. // classes
  109. const classes = getQueryStyles();
  110. // refs
  111. const editorEl = useRef<HTMLDivElement>(null);
  112. const editor = useRef<EditorView>();
  113. const onChangeRef = useRef(onChange);
  114. const dataRef = useRef(data);
  115. const fieldRef = useRef(field);
  116. const themeCompartment = new Compartment();
  117. // get validator
  118. const validator = Validator[field.data_type as keyof typeof Validator];
  119. useEffect(() => {
  120. // update dataRef and onChangeRef when data changes
  121. dataRef.current = data;
  122. onChangeRef.current = onChange;
  123. fieldRef.current = field;
  124. if (editor.current) {
  125. // only data replace should trigger this, otherwise, let cm handle the state
  126. if (editor.current.state.doc.toString() !== data) {
  127. editor.current.dispatch({
  128. changes: {
  129. from: 0,
  130. to: editor.current.state.doc.length,
  131. insert: data,
  132. },
  133. });
  134. }
  135. }
  136. }, [JSON.stringify(searchParams)]);
  137. // create editor
  138. useEffect(() => {
  139. if (!editor.current) {
  140. const startState = EditorState.create({
  141. doc: data,
  142. extensions: [
  143. minimalSetup,
  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. keymap.of([{ key: 'Tab', run: insertTab }]), // fix tab behaviour
  175. indentUnit.of(' '), // fix tab indentation
  176. EditorView.theme({
  177. '&.cm-editor': {
  178. '&.cm-focused': {
  179. outline: 'none',
  180. },
  181. },
  182. '.cm-content': {
  183. fontSize: '12px',
  184. minHeight: '124px',
  185. },
  186. '.cm-gutters': {
  187. display: 'none',
  188. },
  189. }),
  190. EditorView.lineWrapping,
  191. EditorView.updateListener.of(update => {
  192. if (update.docChanged) {
  193. const text = update.state.doc.toString();
  194. const { valid } = validator(text, fieldRef.current);
  195. if (valid || text === '') {
  196. onChangeRef.current(searchParams.anns_field, text);
  197. }
  198. }
  199. if (update.focusChanged) {
  200. setIsFocused(update.view.hasFocus);
  201. }
  202. }),
  203. ],
  204. });
  205. const view = new EditorView({
  206. state: startState,
  207. parent: editorEl.current!,
  208. });
  209. editor.current = view;
  210. return () => {
  211. view.destroy();
  212. editor.current = undefined;
  213. };
  214. }
  215. }, [JSON.stringify(field)]);
  216. useEffect(() => {
  217. // dispatch dark mode change to editor
  218. if (editor.current) {
  219. editor.current.dispatch({
  220. effects: themeCompartment.reconfigure(
  221. themeCompartment.of(theme.palette.mode === 'light' ? githubLight : githubDark)
  222. ),
  223. });
  224. }
  225. }, [theme.palette.mode]);
  226. return (
  227. <div
  228. className={`${classes.vectorInputBox} ${isFocused ? 'focused' : ''}`}
  229. ref={editorEl}
  230. onClick={() => {
  231. if (editor.current) editor.current.focus();
  232. }}
  233. ></div>
  234. );
  235. }