Выбор тегов

Сделай компонент выбора тегов для статьи: пользователь набирает теги в одном поле, каждый введённый тег превращается в удаляемую метку. Выбранные теги показывай под полем и отдавай наружу через колбэк onChange.

tag-picker__none__claude-code__claude-opus-5__1 · 23.09.2026, 19:52:01 мск

Есть проблемы
Без контекста · claude-opus-5 · Claude Code
Контекст до задачи~154
Токенов на прогон10 403
Цена$0.052
Ошибок компилятора3

Первый вызов модели получил 8 579 токенов входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.

Компилятор

  • Строка 2 · TS2305
    Module '"@jinx-ui/react"' has no exported member 'Stack'.
  • Строка 2 · TS2305
    Module '"@jinx-ui/react"' has no exported member 'Tag'.
  • Строка 2 · TS2305
    Module '"@jinx-ui/react"' has no exported member 'TextField'.
Выдуманные компоненты
StackTagTextField

Линтер и покрытие

  • inline-styleСтроки 64, 77
    Инлайновые стили вместо классов и токенов библиотеки
Компоненты библиотеки

Ни одного: код написан без библиотеки, поэтому прогон не засчитан

Ожидались JxTagInput · покрытие 0%

Контекст

Только формулировка задачи

Вход 2 · выход 1 824 · из кэша 8 233 · в кэш 344 · 22 с

Рендер

Код компонента

tsx89 строк
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { useState, useCallback, type KeyboardEvent, type ChangeEvent } from 'react';
import { Stack, Tag, TextField } from '@jinx-ui/react';

// API names (TextField / Tag.onRemove / Stack) assumed from Jinx UI conventions — verify against the installed version.

type ArticleTagPickerProps = {
  label?: string;
  placeholder?: string;
  initialTags?: string[];
  maxTags?: number;
  onChange?: (tags: string[]) => void;
};

function ArticleTagPicker({
  label = 'Теги статьи',
  placeholder = 'Введите тег и нажмите Enter',
  initialTags = [],
  maxTags = 10,
  onChange,
}: ArticleTagPickerProps) {
  const [tags, setTags] = useState<string[]>(initialTags);
  const [draft, setDraft] = useState('');

  const commit = useCallback(
    (raw: string) => {
      const next = raw.trim().replace(/\s+/g, ' ');
      if (!next) return;
      if (tags.length >= maxTags) return;
      if (tags.some((tag) => tag.toLowerCase() === next.toLowerCase())) {
        setDraft('');
        return;
      }
      const updated = [...tags, next];
      setTags(updated);
      setDraft('');
      onChange?.(updated);
    },
    [tags, maxTags, onChange],
  );

  const remove = useCallback(
    (target: string) => {
      const updated = tags.filter((tag) => tag !== target);
      setTags(updated);
      onChange?.(updated);
    },
    [tags, onChange],
  );

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === 'Enter' || event.key === ',') {
      event.preventDefault();
      commit(draft);
      return;
    }
    if (event.key === 'Backspace' && draft === '' && tags.length > 0) {
      remove(tags[tags.length - 1]);
    }
  };

  const isFull = tags.length >= maxTags;

  return (
    <Stack direction="column" style={{ gap: 'var(--jx-space-2)' }}>
      <TextField
        label={label}
        value={draft}
        placeholder={isFull ? `Достигнут предел: ${maxTags}` : placeholder}
        disabled={isFull}
        hint={`${tags.length} из ${maxTags}`}
        onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft(event.target.value)}
        onKeyDown={handleKeyDown}
        onBlur={() => commit(draft)}
      />

      {tags.length > 0 && (
        <Stack direction="row" wrap style={{ gap: 'var(--jx-space-1)' }}>
          {tags.map((tag) => (
            <Tag key={tag} onRemove={() => remove(tag)} removeLabel={`Удалить тег ${tag}`}>
              {tag}
            </Tag>
          ))}
        </Stack>
      )}
    </Stack>
  );
}

export default ArticleTagPicker;