Skip to content
This repository was archived by the owner on Jan 2, 2025. It is now read-only.

Commit 9d7adba

Browse files
Support alternative input type for Chinese users (#1224)
* support alternative input type for chinese users * add input type to settings to allow users to choose input type
1 parent 4bbd4dc commit 9d7adba

23 files changed

Lines changed: 527 additions & 41 deletions

File tree

client/src/Project/CurrentTabContent/ChatTab/ChatPersistentState.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import { focusInput } from '../../../utils/domUtils';
2626
import { ChatsContext } from '../../../context/chatsContext';
2727
import { TabsContext } from '../../../context/tabsContext';
2828
import { getConversation } from '../../../services/api';
29+
import {
30+
concatenateParsedQuery,
31+
splitUserInputAfterAutocomplete,
32+
} from '../../../utils';
2933

3034
type Options = {
3135
path: string;
@@ -190,6 +194,11 @@ const ChatPersistentState = ({
190194

191195
const setInputValueImperatively = useCallback(
192196
(value: ParsedQueryType[] | string) => {
197+
setInputValue(
198+
typeof value === 'string'
199+
? { plain: value, parsed: splitUserInputAfterAutocomplete(value) }
200+
: { parsed: value, plain: concatenateParsedQuery(value) },
201+
);
193202
setInputImperativeValue({
194203
type: 'paragraph',
195204
content:

client/src/Project/CurrentTabContent/ChatTab/Input/InputCore.tsx renamed to client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import { schema as basicSchema } from 'prosemirror-schema-basic';
1414
// @ts-ignore
1515
import * as icons from 'file-icons-js';
1616
import { useTranslation } from 'react-i18next';
17-
import { InputEditorContent, ParsedQueryType } from '../../../../types/general';
18-
import { getFileExtensionForLang } from '../../../../utils';
19-
import { blurInput } from '../../../../utils/domUtils';
20-
import { MentionOptionType } from '../../../../types/results';
17+
import {
18+
InputEditorContent,
19+
ParsedQueryType,
20+
} from '../../../../../types/general';
21+
import { getFileExtensionForLang } from '../../../../../utils';
22+
import { blurInput } from '../../../../../utils/domUtils';
23+
import { MentionOptionType } from '../../../../../types/results';
2124
import { getMentionsPlugin } from './mentionPlugin';
2225
import { addMentionNodes, mapEditorContentToInputValue } from './utils';
2326
import { placeholderPlugin } from './placeholderPlugin';

client/src/Project/CurrentTabContent/ChatTab/Input/mentionPlugin.ts renamed to client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/mentionPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Plugin, PluginKey } from 'prosemirror-state';
22
import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
33
import { ResolvedPos } from 'prosemirror-model';
4-
import { MentionOptionType } from '../../../../types/results';
4+
import { MentionOptionType } from '../../../../../types/results';
55

66
export function getRegexp(mentionTrigger: string, allowSpace?: boolean) {
77
return allowSpace

client/src/Project/CurrentTabContent/ChatTab/Input/nodes.ts renamed to client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/nodes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-ignore
22
import * as icons from 'file-icons-js';
33
import { type AttributeSpec, type NodeSpec } from 'prosemirror-model';
4-
import { getFileExtensionForLang, splitPath } from '../../../../utils';
4+
import { getFileExtensionForLang, splitPath } from '../../../../../utils';
55

66
export const mentionNode: NodeSpec = {
77
group: 'inline',

client/src/Project/CurrentTabContent/ChatTab/Input/placeholderPlugin.ts renamed to client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/placeholderPlugin.ts

File renamed without changes.

client/src/Project/CurrentTabContent/ChatTab/Input/utils.ts renamed to client/src/Project/CurrentTabContent/ChatTab/Input/ProseMirror/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type NodeSpec } from 'prosemirror-model';
33
import {
44
InputEditorContent,
55
ParsedQueryTypeEnum,
6-
} from '../../../../types/general';
6+
} from '../../../../../types/general';
77
import { mentionNode } from './nodes';
88

99
export function addMentionNodes(nodes: OrderedMap<NodeSpec>) {
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import React, {
2+
memo,
3+
ReactNode,
4+
useCallback,
5+
useEffect,
6+
useRef,
7+
useState,
8+
} from 'react';
9+
import {
10+
Mention,
11+
MentionsInput,
12+
OnChangeHandlerFunc,
13+
SuggestionDataItem,
14+
} from 'react-mentions';
15+
import { Trans, useTranslation } from 'react-i18next';
16+
import { getFileExtensionForLang, splitPath } from '../../../../../utils';
17+
import FileIcon from '../../../../../components/FileIcon';
18+
import { FolderIcon, RepositoryIcon } from '../../../../../icons';
19+
import { ParsedQueryType } from '../../../../../types/general';
20+
import { blurInput } from '../../../../../utils/domUtils';
21+
import { MentionOptionType } from '../../../../../types/results';
22+
23+
type Props = {
24+
placeholder: string;
25+
getDataLang: (s: string) => Promise<MentionOptionType[]>;
26+
getDataPath: (s: string) => Promise<MentionOptionType[]>;
27+
getDataRepo: (s: string) => Promise<MentionOptionType[]>;
28+
value?: { parsed: ParsedQueryType[]; plain: string };
29+
onChange: (v: string) => void;
30+
onSubmit: (v: { parsed: ParsedQueryType[]; plain: string }) => void;
31+
isDisabled?: boolean;
32+
};
33+
34+
const inputStyle = {
35+
'&multiLine': {
36+
highlighter: {
37+
maxHeight: 300,
38+
overflow: 'auto',
39+
},
40+
input: {
41+
maxHeight: 300,
42+
overflow: 'auto',
43+
outline: 'none',
44+
},
45+
},
46+
suggestions: {
47+
list: {
48+
maxHeight: '40vh',
49+
overflowY: 'auto',
50+
backgroundColor: 'rgb(var(--bg-shade))',
51+
border: '1px solid rgb(var(--bg-border))',
52+
boxShadow: 'var(--shadow-high)',
53+
padding: 4,
54+
zIndex: 100,
55+
borderRadius: 6,
56+
marginTop: 6,
57+
},
58+
},
59+
};
60+
61+
const ReactMentionsInput = ({
62+
placeholder,
63+
onSubmit,
64+
onChange,
65+
getDataPath,
66+
getDataRepo,
67+
getDataLang,
68+
value,
69+
isDisabled,
70+
}: Props) => {
71+
const { t } = useTranslation();
72+
const inputRef = useRef<HTMLTextAreaElement>(null);
73+
const [isComposing, setComposition] = useState(false);
74+
75+
useEffect(() => {
76+
if (inputRef.current) {
77+
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
78+
inputRef.current.style.height = '56px';
79+
const scrollHeight = inputRef.current.scrollHeight;
80+
81+
// We then set the height directly, outside of the render loop
82+
// Trying to set this with state or a ref will product an incorrect value.
83+
inputRef.current.style.height =
84+
Math.max(Math.min(scrollHeight, 300), 56) + 'px';
85+
}
86+
}, [inputRef.current, value]);
87+
88+
const handleKeyDown = useCallback(
89+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
90+
if (isComposing) {
91+
return true;
92+
}
93+
if (e.key === 'Enter' && !e.shiftKey && onSubmit && value) {
94+
e.preventDefault();
95+
blurInput();
96+
onSubmit({
97+
plain: value.plain
98+
.replace(/\|(repo:.*?)\|/, '$1')
99+
.replace(/\|(path:.*?)\|/, '$1')
100+
.replace(/\|(lang:.*?)\|/, '$1'),
101+
parsed: value.parsed,
102+
});
103+
}
104+
},
105+
[isComposing, onSubmit, value],
106+
);
107+
108+
const repoTransform = useCallback((id: string, trans: string) => {
109+
const split = splitPath(trans);
110+
return trans.startsWith('local//')
111+
? split.slice(-1)[0]
112+
: split.slice(-2).join('/');
113+
}, []);
114+
115+
const pathTransform = useCallback((id: string, trans: string) => {
116+
const split = splitPath(trans);
117+
return `${split[split.length - 1] || split[split.length - 2]}`;
118+
}, []);
119+
120+
const onCompositionStart = useCallback(() => {
121+
setComposition(true);
122+
}, []);
123+
124+
const onCompositionEnd = useCallback(() => {
125+
// this event comes before keydown and sets state faster causing unintentional submit
126+
setTimeout(() => setComposition(false), 10);
127+
}, []);
128+
129+
const handleChange = useCallback<OnChangeHandlerFunc>(
130+
(e) => {
131+
onChange(e.target.value);
132+
},
133+
[onChange],
134+
);
135+
136+
const renderRepoSuggestion = useCallback(
137+
(
138+
entry: SuggestionDataItem,
139+
search: string,
140+
highlightedDisplay: ReactNode,
141+
index: number,
142+
focused: boolean,
143+
) => {
144+
const d = entry as MentionOptionType;
145+
return (
146+
<div>
147+
{d.isFirst ? (
148+
<div className="flex items-center rounded-6 gap-2 px-2 py-1 text-label-muted caption cursor-default">
149+
<Trans>Repositories</Trans>
150+
</div>
151+
) : null}
152+
<div
153+
className={`flex items-center justify-between rounded-6 gap-2 px-2 h-8 ${
154+
focused ? 'bg-bg-base-hover' : ''
155+
} body-s text-label-title cursor-pointer max-w-[600px] ellipsis`}
156+
>
157+
<span className="flex items-center gap-2">
158+
<RepositoryIcon sizeClassName="w-4 h-4" />
159+
<span className="ellipsis text-left ">{d.display}</span>
160+
</span>
161+
<span className="ellipsis text-label-muted text-left body-mini">
162+
{d.hint}
163+
</span>
164+
</div>
165+
</div>
166+
);
167+
},
168+
[],
169+
);
170+
171+
const renderPathSuggestion = useCallback(
172+
(
173+
entry: SuggestionDataItem,
174+
search: string,
175+
highlightedDisplay: ReactNode,
176+
index: number,
177+
focused: boolean,
178+
) => {
179+
const d = entry as MentionOptionType;
180+
return (
181+
<div>
182+
{d.isFirst ? (
183+
<div className="flex items-center rounded-6 gap-2 px-2 py-1 text-label-muted caption cursor-default">
184+
<Trans>{d.type === 'dir' ? 'Directories' : 'Files'}</Trans>
185+
</div>
186+
) : null}
187+
<div
188+
className={`flex items-center justify-between rounded-6 gap-2 px-2 h-8 ${
189+
focused ? 'bg-bg-base-hover' : ''
190+
} body-s text-label-title cursor-pointer max-w-[600px] ellipsis`}
191+
>
192+
<span className="flex items-center gap-2">
193+
{d.type === 'dir' ? (
194+
<FolderIcon sizeClassName="w-4 h-4" />
195+
) : (
196+
<FileIcon filename={d.display} />
197+
)}
198+
<span className="ellipsis text-left ">{d.display}</span>
199+
</span>
200+
<span className="ellipsis text-label-muted text-left body-mini">
201+
{d.hint}
202+
</span>
203+
</div>
204+
</div>
205+
);
206+
},
207+
[],
208+
);
209+
210+
const renderLangSuggestion = useCallback(
211+
(
212+
entry: SuggestionDataItem,
213+
search: string,
214+
highlightedDisplay: ReactNode,
215+
index: number,
216+
focused: boolean,
217+
) => {
218+
const d = entry as MentionOptionType;
219+
return (
220+
<div>
221+
{d.isFirst ? (
222+
<div className="flex items-center rounded-6 gap-2 px-2 py-1 text-label-muted caption cursor-default">
223+
<Trans>Languages</Trans>
224+
</div>
225+
) : null}
226+
<div
227+
className={`flex items-center justify-between rounded-6 gap-2 px-2 h-8 ${
228+
focused ? 'bg-bg-base-hover' : ''
229+
} body-s text-label-title cursor-pointer max-w-[600px] ellipsis`}
230+
>
231+
<span className="flex items-center gap-2">
232+
<FileIcon filename={getFileExtensionForLang(d.display, true)} />
233+
<span className="ellipsis text-left ">{d.display}</span>
234+
</span>
235+
<span className="ellipsis text-label-muted text-left body-mini">
236+
{d.hint}
237+
</span>
238+
</div>
239+
</div>
240+
);
241+
},
242+
[],
243+
);
244+
245+
return (
246+
<div className="w-full body-base pb-4 !leading-[24px] bg-transparent outline-none focus:outline-0 resize-none flex-grow-0 flex flex-col justify-center">
247+
<MentionsInput
248+
value={value?.plain || ''}
249+
// id={id}
250+
onChange={handleChange}
251+
className={`ReactMention w-full bg-transparent rounded-lg outline-none focus:outline-0 resize-none
252+
placeholder:text-current flex-grow-0`}
253+
placeholder={placeholder}
254+
inputRef={inputRef}
255+
disabled={isDisabled}
256+
onCompositionStart={onCompositionStart}
257+
onCompositionEnd={onCompositionEnd}
258+
// @ts-ignore
259+
onKeyDown={handleKeyDown}
260+
// onFocus={handleInputFocus}
261+
style={inputStyle}
262+
>
263+
<Mention
264+
trigger="@"
265+
markup="|repo:__id__|"
266+
data={getDataRepo}
267+
renderSuggestion={renderRepoSuggestion}
268+
className="relative before:bg-bg-shade-hover before:rounded before:absolute before:-top-0.5 before:-bottom-0.5 before:-left-1 before:-right-0.5"
269+
appendSpaceOnAdd
270+
displayTransform={repoTransform}
271+
/>
272+
<Mention
273+
trigger="@"
274+
markup="|path:__id__|"
275+
data={getDataPath}
276+
renderSuggestion={renderPathSuggestion}
277+
className="relative before:bg-bg-shade-hover before:rounded before:absolute before:-top-0.5 before:-bottom-0.5 before:-left-1 before:-right-0.5"
278+
appendSpaceOnAdd
279+
displayTransform={pathTransform}
280+
/>
281+
<Mention
282+
trigger="@"
283+
markup="|lang:__id__|"
284+
data={getDataLang}
285+
appendSpaceOnAdd
286+
renderSuggestion={renderLangSuggestion}
287+
className="relative before:bg-bg-shade-hover before:rounded before:absolute before:-top-0.5 before:-bottom-0.5 before:-left-1 before:-right-0.5"
288+
/>
289+
</MentionsInput>
290+
</div>
291+
);
292+
};
293+
294+
export default memo(ReactMentionsInput);

0 commit comments

Comments
 (0)