Skip to content

Commit 914df1d

Browse files
committed
feat: add character counter to AI and project description fields
Users had no way to see how many characters they had entered or what the limit was in AI language notes, AI project description, and the project settings description. Reuse the counter from key creation: - move CharacterCounter to tg.component/common, since it is no longer owned by the translations view - drop the "description is too long" helper text in the AI dialogs, as the counter already renders the over-limit warning - pass the over-limit count as a number instead of a string, so ICU plural selection actually applies Closes #3833
1 parent 2940a13 commit 914df1d

10 files changed

Lines changed: 76 additions & 37 deletions

File tree

e2e/cypress/e2e/languages/aiCustomization.cy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('Machine translation settings', () => {
2525
const description = 'Tolgee is software localization platform';
2626
cy.gcy('ai-customization-project-description-add').click();
2727
cy.gcy('project-ai-prompt-dialog-description-input').type(description);
28+
cy.gcy('character-counter').should('contain', `${description.length}/2000`);
2829
cy.gcy('project-ai-prompt-dialog-save').click();
2930
waitForGlobalLoading();
3031
cy.gcy('ai-customization-project-description')
@@ -39,6 +40,7 @@ describe('Machine translation settings', () => {
3940
language: 'cs',
4041
}).click();
4142
cy.gcy('language-ai-prompt-dialog-description-input').type(description);
43+
cy.gcy('character-counter').should('contain', `${description.length}/2000`);
4244
cy.gcy('language-ai-prompt-dialog-save').click();
4345
waitForGlobalLoading();
4446
gcyAdvanced({

e2e/cypress/e2e/projects/settings.cy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ describe('Projects Basics', () => {
2828
.first()
2929
.type('Test description');
3030

31+
cy.gcy('character-counter').should(
32+
'contain',
33+
`${'Test description'.length}/2000`
34+
);
35+
3136
cy.gcy('default-namespace-select').should('not.exist');
3237

3338
cy.gcy('global-form-save-button').click();

e2e/cypress/support/dataCyType.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ declare namespace DataCy {
136136
"branch-selector": true;
137137
"cell-key-screenshot-dropzone": true;
138138
"cell-key-screenshot-file-input": true;
139+
"character-counter": true;
139140
"checkbox-group-multiselect": true;
140141
"color-palette-field": true;
141142
"color-palette-popover": true;

webapp/src/views/projects/translations/cell/CharacterCounter.tsx renamed to webapp/src/component/common/CharacterCounter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ export const CharacterCounter: React.FC<React.PropsWithChildren<Props>> = ({
3737
const overLimit = currentCount > maxLimit;
3838

3939
return (
40-
<StyledContainer>
40+
<StyledContainer data-cy="character-counter">
4141
{overLimit && (
4242
<StyledWarning role="alert" aria-atomic="true">
4343
{t('translation_char_counter_over_limit', {
44-
count: String(currentCount - maxLimit),
44+
count: currentCount - maxLimit,
4545
})}
4646
</StyledWarning>
4747
)}

webapp/src/constants/GlobalValidationSchema.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export type TranslateFunction = TFnType<
1616
type AccountType =
1717
components['schemas']['PrivateUserAccountModel']['accountType'];
1818

19+
export const PROJECT_DESCRIPTION_MAX_LENGTH = 2000;
20+
1921
Yup.setLocale({
2022
// use constant translation keys for messages without values
2123
mixed: {
@@ -258,7 +260,10 @@ export class Validation {
258260

259261
static readonly PROJECT_SETTINGS = Yup.object().shape({
260262
name: Yup.string().trim().required().min(3).max(100),
261-
description: Yup.string().nullable().min(3).max(2000),
263+
description: Yup.string()
264+
.nullable()
265+
.min(3)
266+
.max(PROJECT_DESCRIPTION_MAX_LENGTH),
262267
});
263268

264269
static readonly TRANSLATION_MEMORY_CREATE_EDIT = Yup.object().shape({

webapp/src/ee/llm/AiContextData/AiLanguageDescriptionDialog.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import { useProject } from 'tg.hooks/useProject';
1515
import { components } from 'tg.service/apiSchema.generated';
1616
import { useApiMutation } from 'tg.service/http/useQueryApi';
1717
import { LanguageItem } from 'tg.component/languages/LanguageItem';
18+
import { CharacterCounter } from 'tg.component/common/CharacterCounter';
1819
import { AiTips } from './AiTips';
20+
import { AI_DESCRIPTION_MAX_LENGTH } from './constants';
1921

2022
type LanguageModel = components['schemas']['LanguageModel'];
2123

@@ -70,7 +72,8 @@ export const AiLanguageDescriptionDialog = ({
7072
);
7173
}
7274

73-
const isTooLong = Boolean(inputValue && inputValue.length > 2000);
75+
const currentCount = inputValue?.length ?? 0;
76+
const isTooLong = currentCount > AI_DESCRIPTION_MAX_LENGTH;
7477

7578
return (
7679
<Dialog open fullWidth maxWidth="sm" onClose={handleClose}>
@@ -82,23 +85,22 @@ export const AiLanguageDescriptionDialog = ({
8285
</Box>
8386
<DialogContent>
8487
<Box sx={{ display: 'grid', gap: '16px' }}>
85-
<TextField
86-
multiline
87-
sx={{ width: '100%', mt: '2px' }}
88-
placeholder={placeholder}
89-
minRows={2}
90-
value={inputValue}
91-
onChange={(e) => setInputValue(e.currentTarget.value)}
92-
error={isTooLong}
93-
helperText={
94-
isTooLong &&
95-
t(
96-
'language_ai_prompt_dialog_description_too_long',
97-
'Description is too long (max 2000 characters)'
98-
)
99-
}
100-
data-cy="language-ai-prompt-dialog-description-input"
101-
/>
88+
<Box display="grid" gap={0.5}>
89+
<TextField
90+
multiline
91+
sx={{ width: '100%', mt: '2px' }}
92+
placeholder={placeholder}
93+
minRows={2}
94+
value={inputValue}
95+
onChange={(e) => setInputValue(e.currentTarget.value)}
96+
error={isTooLong}
97+
data-cy="language-ai-prompt-dialog-description-input"
98+
/>
99+
<CharacterCounter
100+
currentCount={currentCount}
101+
maxLimit={AI_DESCRIPTION_MAX_LENGTH}
102+
/>
103+
</Box>
102104
<AiTips
103105
tips={[
104106
t('language_ai_prompt_tip_usage'),

webapp/src/ee/llm/AiContextData/AiProjectDescriptionDialog.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Box,
23
Button,
34
Dialog,
45
DialogActions,
@@ -9,10 +10,12 @@ import {
910
import { useTranslate } from '@tolgee/react';
1011
import { useState } from 'react';
1112
import LoadingButton from 'tg.component/common/form/LoadingButton';
13+
import { CharacterCounter } from 'tg.component/common/CharacterCounter';
1214
import { confirmDiscardUnsaved } from 'tg.hooks/confirmation';
1315
import { useProject } from 'tg.hooks/useProject';
1416
import { useApiMutation } from 'tg.service/http/useQueryApi';
1517
import { AiTips } from './AiTips';
18+
import { AI_DESCRIPTION_MAX_LENGTH } from './constants';
1619

1720
export const EXAMPLE = 'App for teaching children about the world.';
1821

@@ -64,25 +67,29 @@ export const AiProjectDescriptionDialog = ({
6467
);
6568
}
6669

67-
const isTooLong = Boolean(inputValue && inputValue?.length > 2000);
70+
const currentCount = inputValue?.length ?? 0;
71+
const isTooLong = currentCount > AI_DESCRIPTION_MAX_LENGTH;
6872

6973
return (
7074
<Dialog open fullWidth maxWidth="sm" onClose={handleClose}>
7175
<DialogTitle>{t('project_ai_prompt_dialog_title')}</DialogTitle>
7276
<DialogContent sx={{ display: 'grid', gap: '16px' }}>
73-
<TextField
74-
multiline
75-
sx={{ width: '100%', mt: '2px' }}
76-
placeholder={placeholder}
77-
minRows={2}
78-
value={inputValue}
79-
onChange={(e) => setInputValue(e.currentTarget.value)}
80-
error={isTooLong}
81-
helperText={
82-
isTooLong && t('project_ai_prompt_dialog_description_too_long')
83-
}
84-
data-cy="project-ai-prompt-dialog-description-input"
85-
/>
77+
<Box display="grid" gap={0.5}>
78+
<TextField
79+
multiline
80+
sx={{ width: '100%', mt: '2px' }}
81+
placeholder={placeholder}
82+
minRows={2}
83+
value={inputValue}
84+
onChange={(e) => setInputValue(e.currentTarget.value)}
85+
error={isTooLong}
86+
data-cy="project-ai-prompt-dialog-description-input"
87+
/>
88+
<CharacterCounter
89+
currentCount={currentCount}
90+
maxLimit={AI_DESCRIPTION_MAX_LENGTH}
91+
/>
92+
</Box>
8693
<AiTips
8794
tips={[
8895
t('project_ai_prompt_dialog_tip_topic'),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const AI_DESCRIPTION_MAX_LENGTH = 2000;

webapp/src/views/projects/project/ProjectSettingsGeneral.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState } from 'react';
2+
import { useField } from 'formik';
23
import { useProjectLanguages } from 'tg.hooks/useProjectLanguages';
34
import { ProjectProfileAvatar } from './ProjectProfileAvatar';
45
import { BaseLanguageSelect } from 'tg.views/projects/project/components/BaseLanguageSelect';
@@ -7,7 +8,11 @@ import { StandardForm } from 'tg.component/common/form/StandardForm';
78
import { useApiMutation } from 'tg.service/http/useQueryApi';
89
import { useProject } from 'tg.hooks/useProject';
910
import { messageService } from 'tg.service/MessageService';
10-
import { Validation } from 'tg.constants/GlobalValidationSchema';
11+
import {
12+
PROJECT_DESCRIPTION_MAX_LENGTH,
13+
Validation,
14+
} from 'tg.constants/GlobalValidationSchema';
15+
import { CharacterCounter } from 'tg.component/common/CharacterCounter';
1116
import LoadingButton from 'tg.component/common/form/LoadingButton';
1217
import { useLeaveProject } from '../useLeaveProject';
1318
import { TextField } from 'tg.component/common/form/fields/TextField';
@@ -59,6 +64,16 @@ const LanguageSelect = () => {
5964
);
6065
};
6166

67+
const DescriptionCharacterCounter = () => {
68+
const [field] = useField<string | undefined>('description');
69+
return (
70+
<CharacterCounter
71+
currentCount={field.value?.length ?? 0}
72+
maxLimit={PROJECT_DESCRIPTION_MAX_LENGTH}
73+
/>
74+
);
75+
};
76+
6277
export const ProjectSettingsGeneral = () => {
6378
const project = useProject();
6479
const { t } = useTranslate();
@@ -201,6 +216,7 @@ export const ProjectSettingsGeneral = () => {
201216
data-cy="project-settings-description"
202217
sx={{ mt: 0 }}
203218
/>
219+
<DescriptionCharacterCounter />
204220
</Box>
205221
<ProjectLanguagesProvider>
206222
<LanguageSelect />

webapp/src/views/projects/translations/translationVisual/PluralEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getLanguageDirection } from 'tg.fixtures/getLanguageDirection';
66
import { RefObject } from 'react';
77
import { EditorView } from 'codemirror';
88
import { useProject } from 'tg.hooks/useProject';
9-
import { CharacterCounter } from '../cell/CharacterCounter';
9+
import { CharacterCounter } from 'tg.component/common/CharacterCounter';
1010
import { getVisibleCharCount } from '../cell/getVisibleCharCount';
1111

1212
type Props = {

0 commit comments

Comments
 (0)