Представьте, что вы пишете сообщение, и приложение не просто предлагает вам следующее слово, а формирует целые осмысленные предложения, учитывая контекст вашей переписки. Или, что еще круче, вы начинаете набирать код в редакторе, и он не только автоматически закрывает скобки, но и предлагает логически верное продолжение функции, учитывая архитектуру вашего проекта.
В React интеграция подобных возможностей становится реальностью благодаря таким мощным инструментам, как API от OpenAI. И нет, я не преувеличиваю – я сам сэкономил часы работы, интегрировав эту технологию в несколько своих проектов.
| JavaScript | 1
2
3
4
5
6
7
8
9
| // Без ИИ-автозаполнения:
<input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
// С ИИ-автозаполнением:
<AutocompleteInput
value={value}
onChange={setValue}
suggestions={aiSuggestions}
/> |
|
Когда я впервые столкнулся с задачей добавить ИИ-автозаполнение в крупный проект на React, я был удивлен, насколько мало было готовых решений и понятных руководств. Мне пришлось собирать информацию по крупицам, экспериментировать с различными подходами и набивать собственные шишки. И теперь я хочу поделиться этим опытом, чтобы сэкономить вам время и нервы.
В основе любого ИИ-автозаполнения лежит, конечно же, языковая модель. В случае с OpenAI это могут быть GPT-3.5, GPT-4 или более специализированные модели. Они обучены на огромных объемах текста и способны генерировать продолжения фраз, учитывая контекст. Но чудо происходит не в самой модели, а в том, как вы интегрируете ее в свое приложение и настраиваете под конкретные задачи. Вот простой пример - вы разрабатываете форму обратной связи, и хотите предложить пользователю готовые варианты завершения его мысли. Раньше мы бы использовали предопределенный набор шаблонов или простое сопоставление с ключевыми словами. Сейчас же можно в реальном времени отправлять начало фразы пользователя в OpenAI и получать осмысленное, контекстно-релевантное продолжение.
Я считаю, что ИИ-автозаполнение - это не просто удобная функция, а полноценное изменение парадигмы взаимодействия с интерфейсами. Мы переходим от реактивных интерфейсов, которые просто отвечают на действия пользователя, к проактивным, которые предугадывают намерения и облегчают взаимодействие. Однако, нельзя не отметить, что это палка о двух концах. С одной стороны, автозаполнение ускоряет работу и делает интерфейс более дружелюбным. С другой - оно может раздражать, если работает некорректно или слишком настойчиво. Да и не будем забывать о приватности данных и о том, что каждый запрос к API имеет свою стоимость.
Технологический контекст интеграции OpenAI в React
Когда я начинал свой путь в веб-разработке, автозаполнение было примитивной штукой. Помню, как мы все радовались, когда Google предложил первые версии автозаполнения поисковых запросов где-то в середине 2000-х. Это было просто статистическое сопоставление с наиболее популярными поисковыми запросами. А что сегодня? Сегодня автозаполнение – это целая наука, основанная на глубоком анализе контекста, персональных предпочтений и даже эмоционального состояния пользователя.
Эволюция технологий автозаполнения
Я выделяю несколько ключевых этапов в эволюции автозаполнения:
1. Статическое автозаполнение – простые предопределенные списки или шаблоны. Типичный пример – автозаполнение в формах с выбором страны.
2. Статистическое автозаполнение – основано на частоте использования фраз или слов. Здесь алгоритм просто предлагает то, что чаще всего выбирают другие пользователи.
3. Контекстное автозаполнение – учитывает не только частоту, но и контекст. Например, автозаполнение в Gmail, которое предлагает разные варианты в зависимости от того, пишете вы деловое или личное письмо.
4. ИИ-автозаполнение – использует нейронные сети для генерации осмысленных предложений, учитывая широкий контекст и даже тональность общения.
Последний этап стал возможен только благодаря прорыву в области языковых моделей. И здесь нельзя не отметить роль OpenAI с их моделями GPT.
Текущее состояние рынка и ключевые игроки
На сегодняшний день рынок ИИ-автозаполнения представлен несколькими крупными игроками:
OpenAI GPT
Безусловно, самый известный и, пожалуй, самый мощный инструмент на рынке. Модели GPT-3.5 и GPT-4 демонстрируют фантастические результаты в генерации текста. API OpenAI позволяет использовать эти модели для автозаполнения в реальном времени. Я сам работал с GPT-4 в нескольких проектах и могу сказать, что его способность понимать контекст просто поражает.
| JavaScript | 1
2
3
4
5
6
7
| // Пример запроса к API OpenAI
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: [INLINE]Complete this: ${prompt}[/INLINE] }],
temperature: 0.7,
max_tokens: 20
}); |
|
Но у OpenAI есть и недостатки – относительно высокая стоимость, особенно для GPT-4, и ограничения по скорости запросов, что критично для автозаполнения в реальном времени.
Google AI (PaLM, Gemini)
Google предлагает свои языковые модели PaLM и недавно выпущенный Gemini. По моему опыту, PaLM API работает немного быстрее, чем GPT, но качество генерации текста немного уступает. Впрочем, для многих задач автозаполнения разница не критична.
| JavaScript | 1
2
3
4
5
6
| // Пример запроса к Google PaLM API
const response = await palm.generate({
prompt: [INLINE]Complete this: ${prompt}[/INLINE],
temperature: 0.7,
maxOutputTokens: 20
}); |
|
Hugging Face и открытые модели
Если вам не нравится зависимость от коммерческих API, то можно обратить внимание на открытые модели, доступные через Hugging Face. Там есть множество моделей разного размера и специализации. Некоторые из них можно даже запустить локально, что решает проблемы с приватностью данных и стоимостью использования. Но, честно говоря, такой подход требует гораздо больше технических знаний и ресурсов. Я пробовал запускать некоторые модели локально, и это требовало серьезных вычислительных мощностей для адекватной производительности.
Anthropic Claude
Относительно новый игрок на рынке, но уже заслуживший внимание благодаря своему подходу к безопасности и этичности ИИ. По моим наблюдениям, Claude иногда генерирует более "осторожные" и "политкорректные" ответы, чем GPT, что может быть как плюсом, так и минусом в зависимости от вашей задачи.
Почему React для интеграции с ИИ?
Теперь давайте поговорим о том, почему React так хорошо подходит для интеграции с ИИ-сервисами автозаполнения.
Во-первых, компонентная структура React делает изоляцию логики автозаполнения очень естественной. Вы можете создать компонент AIAutocomplete, который будет отвечать за всю логику взаимодействия с API и отображение предложений.
Во-вторых, реактивная природа React отлично подходит для обработки асинхронных событий, таких как получение предложений от API. Вы можете использовать хуки useState и useEffect для управления состоянием и сайд-эффектами, связанными с автозаполнением.
| JavaScript | 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
| function AIAutocomplete({ value, onChange }) {
const [suggestions, setSuggestions] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (value.length < 3) {
setSuggestions([]);
return;
}
const getSuggestions = async () => {
setLoading(true);
try {
const result = await fetchSuggestionsFromAI(value);
setSuggestions(result);
} catch (error) {
console.error('Ошибка при получении предложений:', error);
setSuggestions([]);
} finally {
setLoading(false);
}
};
const debounceTimer = setTimeout(getSuggestions, 300);
return () => clearTimeout(debounceTimer);
}, [value]);
// Рендеринг компонента
} |
|
В-третьих, экосистема React предлагает множество библиотек для работы с формами, управления состоянием и оптимизации производительности, что критично для создания отзывчивого интерфейса автозаполнения.
Сравнительный анализ решений для ИИ-автозаполнения
Раз уж мы заговорили о разных провайдерах ИИ-сервисов, давайте проведем небольшой сравнительный анализ. Я не раз сталкивался с ситуацией, когда приходилось выбирать между разными API для конкретного проекта, и знаю, насколько важно сделать правильный выбор.
Скорость ответа
Скорость имеет решающее значение для автозаполнения - пользователь не будет ждать несколько секунд, пока появится подсказка. По моим замерам:
OpenAI API: первый ответ обычно приходит через 200-500 мс, что немного медленее, чем хотелось бы для интерактивного автозаполнения;
Google PaLM: 150-400 мс в среднем, немного быстрее OpenAI;
Anthropic Claude: 300-600 мс, один из самых медленных вариантов;
Локальные модели: 50-200 мс при достаточной вычислительной мощности, явный лидер по скорости;
Конечно, эти цифры сильно зависят от размера модели, настроек запроса и сетевой задержки. Я рекомендую делать собственные замеры для конкретного случая.
Качество генерации
Здесь, как ни крути, OpenAI пока впереди планеты всей. Особенно GPT-4 показывает удивительные результаты в понимании контекста и генерации осмысленных продолжений. Но для простых задач автозаполнения разница может быть не так заметна, и более легковесные модели от Google или локальные решения могут быть вполне достаточны. Вот небольшой эксперимент, который я провел для себя. Фраза: "Наша компания стремится улучшить процесс..."
GPT-4: "...взаимодействия с клиентами путем внедрения инновационных технологий и персонализированного подхода."
PaLM: "...обслуживания клиентов и повысить эффективность работы."
Claude: "...коммуникации между отделами для более эффективного решения проблем клиентов."
Видите разницу? GPT-4 дает более развернутый и контекстно богатый вариант, тогда как остальные модели предлагают более общие фразы.
Стоимость использования
Это, пожалуй, самый болезненный вопрос при выборе ИИ-провайдера:
OpenAI: примерно $0.01-0.10 за 1000 токенов для GPT-4, дешевле для GPT-3.5,
Google PaLM: сопоставимо с OpenAI, иногда немного дешевле,
Anthropic Claude: обычно дороже OpenAI,
Локальные модели: только единоразовые затраты на инфраструктуру.
Для высоконагруженных приложений с интенсивным использованием автозаполнения счета могут быстро расти. Я сталкивался с проектом, где месячные расходы на API OpenAI достигали нескольких тысяч долларов.
Ограничения и лимиты API
Это то, о чем часто забывают при планировании, но сталкиваются при реализации:
OpenAI: лимитирует количество запросов в минуту, что может стать проблемой при интенсивном использовании
Google PaLM: похожие ограничения, но в моем опыте немного более гибкие квоты
Anthropic Claude: строгие лимиты для бесплатных аккаунтов
Локальные модели: ограничены только вашими вычислительными мощностями
Для решения проблемы лимитов я часто использую паттерн с очередью запросов и локальным кэшированием результатов. Это позволяет снизить нагрузку на API и обеспечить плавную работу даже при достижении лимитов.
Интеграция OpenAI с React: технические аспекты
Теперь, когда мы разобрались с общим ландшафтом, давайте углубимся в технические аспекты интеграции OpenAI с React. Я выделяю несколько ключевых моментов:
Прокси-сервер для защиты API-ключей
Первое и самое важное правило - никогда не хранить API-ключи на фронтенде. Это огромная дыра в безопасности. Вместо этого я всегда создаю простой прокси-сервер, который принимает запросы от клиента и перенаправляет их к API, добавляя ключ. Это может быть простой Express-сервер:
| JavaScript | 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
| const express = require('express');
const cors = require('cors');
const { OpenAI } = require('openai');
const app = express();
app.use(cors());
app.use(express.json());
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
app.post('/api/autocomplete', async (req, res) => {
const { prompt } = req.body;
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: [INLINE]Complete this: ${prompt}[/INLINE] }],
temperature: 0.7,
max_tokens: 20
});
res.json({ completion: completion.choices[0].message.content.trim() });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Что-то пошло не так' });
}
});
app.listen(5000, () => console.log('Сервер запущен на http://localhost:5000')); |
|
Управление состоянием запросов
В React-приложении нам нужно эффективно управлять состоянием запросов, обрабатывать загрузку, ошибки и результаты. Я часто использую для этого хуки, а иногда и библиотеки типа React Query или SWR, которые значительно упрощают работу с асинхронными запросами. Вот пример кастомного хука для работы с автозаполнением:
| JavaScript | 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
| function useAIAutocomplete(inputValue, options = {}) {
const [suggestions, setSuggestions] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { debounceTime = 300, minLength = 3 } = options;
useEffect(() => {
if (inputValue.length < minLength) {
setSuggestions('');
return;
}
const debounceFn = setTimeout(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/autocomplete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: inputValue })
});
if (!response.ok) throw new Error('Ошибка сети');
const data = await response.json();
setSuggestions(data.completion);
} catch (err) {
setError(err.message);
setSuggestions('');
} finally {
setIsLoading(false);
}
}, debounceTime);
return () => clearTimeout(debounceFn);
}, [inputValue, debounceTime, minLength]);
return { suggestions, isLoading, error };
} |
|
Telethon/openai. почему GPT-3 от openAi пишет полный бред, и отвечает на свои же сообщения? Суть проблемы: Решил я значит поселить на аккаунт телеграма GPT-3 нейронку, чтоб за меня на... Openai API: You tried to access openai.ChatCompletion, but this is no longer supported import openai
openai.api_key =... Something went wrong. If this issue persists please contact us through our help center at help.openai.com Друзья, доброго дня! Я зарегался на чатджпити https://chat.openai.com/chat. с анг. номером и бесп.... AttributeError: module 'openai' has no attribute 'ChatCompletion' import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
completion =...
Архитектурные решения для ИИ-автозаполнения
Теперь, когда мы понимаем технологический ландшафт, давайте погрузимся в самое интересное – архитектурные решения. Я не раз ломал голову над тем, как правильно организовать взаимодействие между React-компонентами и ИИ-сервисами, и поверьте, это та еще головоломка.
Паттерны проектирования для ИИ-автозаполнения
Существует несколько подходов к архитектуре компонентов автозаполнения, и каждый имеет свои преимущества и недостатки.
Контролируемый компонент с выделенным состоянием
Это, пожалуй, самый распространенный и интуитивно понятный подход. Вы создаете компонент ввода, который управляет своим внутренним состоянием и взаимодействует с ИИ-сервисом.
| JavaScript | 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
| function AIInput() {
const [input, setInput] = useState('');
const [suggestions, setSuggestions] = useState('');
useEffect(() => {
const fetchSuggestions = async () => {
// Запрос к ИИ-сервису
};
if (input.length > 2) {
const timerId = setTimeout(fetchSuggestions, 300);
return () => clearTimeout(timerId);
}
}, [input]);
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{suggestions && (
<div className="suggestions">{suggestions}</div>
)}
</div>
);
} |
|
Этот подход хорош своей простотой, но у него есть серьезный недостаток – тесная связь между логикой ИИ и компонентом ввода. Если вам нужно переиспользовать эту логику в другом месте, придется дублировать код.
Паттерн "Компоновщик" (Composition Pattern)
Более гибкий подход – разделить логику автозаполнения и компонент ввода. Я создаю отдельный хук useAIAutocomplete, который может быть использован с любым компонентом ввода.
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Хук для ИИ-автозаполнения
function useAIAutocomplete(value, options) {
const [suggestions, setSuggestions] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Логика запросов к ИИ
return { suggestions, isLoading };
}
// Компонент ввода
function AutocompleteInput({ value, onChange }) {
const { suggestions, isLoading } = useAIAutocomplete(value);
return (
<div>
<input value={value} onChange={onChange} />
{isLoading && <div>Загрузка...</div>}
{!isLoading && suggestions && (
<div className="suggestions">{suggestions}</div>
)}
</div>
);
} |
|
Этот подход значительно улучшает переиспользуемость и тестируемость кода. Логика автозаполнения теперь полностью отделена от компонента ввода.
Паттерн "Наблюдатель" (Observer Pattern)
Для более сложных случаев, когда несколько компонентов должны реагировать на изменения в автозаполнении, я рекомендую использовать паттерн "Наблюдатель". Это можно реализовать с помощью контекста React или сторонних решений для управления состоянием.
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Создаем контекст для автозаполнения
const AutocompleteContext = createContext();
function AutocompleteProvider({ children }) {
const [state, dispatch] = useReducer(autocompleteReducer, initialState);
// Методы для работы с автозаполнением
const getSuggestions = async (input) => {
dispatch({ type: 'FETCH_START' });
try {
const suggestions = await fetchFromAI(input);
dispatch({ type: 'FETCH_SUCCESS', payload: suggestions });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error });
}
};
return (
<AutocompleteContext.Provider value={{ state, getSuggestions }}>
{children}
</AutocompleteContext.Provider>
);
} |
|
Этот подход особенно полезен, когда логика автозаполнения должна быть доступна в разных частях приложения или когда вам нужно реализовать сложную логику обработки предложений.
Интеграция с популярными стейт-менеджерами
В реальных проектах я редко использую только внутреннее состояние React. Обычно для управления состоянием приложения применяются специализированные библиотеки. Давайте рассмотрим, как интегрировать ИИ-автозаполнение с некоторыми из них.
Redux Toolkit
Redux Toolkit стал стандартом для работы с Redux. Для интеграции ИИ-автозаполнения я создаю отдельный слайс:
| JavaScript | 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
| // autocompleteSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchSuggestions = createAsyncThunk(
'autocomplete/fetchSuggestions',
async (input, { rejectWithValue }) => {
try {
const response = await fetch('/api/autocomplete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: input })
});
if (!response.ok) throw new Error('Ошибка сети');
const data = await response.json();
return data.completion;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const autocompleteSlice = createSlice({
name: 'autocomplete',
initialState: {
suggestions: '',
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
},
reducers: {
clearSuggestions: (state) => {
state.suggestions = '';
}
},
extraReducers: (builder) => {
builder
.addCase(fetchSuggestions.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchSuggestions.fulfilled, (state, action) => {
state.status = 'succeeded';
state.suggestions = action.payload;
})
.addCase(fetchSuggestions.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
}
});
export const { clearSuggestions } = autocompleteSlice.actions;
export default autocompleteSlice.reducer; |
|
Теперь мы можем использовать этот слайс в наших компонентах:
| JavaScript | 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
| function AutocompleteInput() {
const [input, setInput] = useState('');
const dispatch = useDispatch();
const { suggestions, status } = useSelector((state) => state.autocomplete);
useEffect(() => {
if (input.length > 2) {
const timerId = setTimeout(() => {
dispatch(fetchSuggestions(input));
}, 300);
return () => clearTimeout(timerId);
} else {
dispatch(clearSuggestions());
}
}, [input, dispatch]);
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{status === 'loading' && <div>Загрузка...</div>}
{status === 'succeeded' && suggestions && (
<div className="suggestions">{suggestions}</div>
)}
</div>
);
} |
|
Redux отлично подходит для сложных приложений с множеством взаимосвязанных состояний, но для простых случаев это может быть излишним.
Zustand
Zustand – мой личный фаворит для управления состоянием в небольших и средних проектах. Он проще Redux, но при этом достаточно мощный для большинства задач.
| JavaScript | 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
| // autocompleteStore.js
import create from 'zustand';
const useAutocompleteStore = create((set) => ({
suggestions: '',
isLoading: false,
error: null,
fetchSuggestions: async (input) => {
if (input.length < 3) {
set({ suggestions: '' });
return;
}
set({ isLoading: true });
try {
const response = await fetch('/api/autocomplete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: input })
});
if (!response.ok) throw new Error('Ошибка сети');
const data = await response.json();
set({ suggestions: data.completion, isLoading: false, error: null });
} catch (error) {
set({ suggestions: '', isLoading: false, error: error.message });
}
},
clearSuggestions: () => set({ suggestions: '' })
}));
export default useAutocompleteStore; |
|
Использование в компоненте:
| JavaScript | 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
| function AutocompleteInput() {
const [input, setInput] = useState('');
const { suggestions, isLoading, fetchSuggestions, clearSuggestions } = useAutocompleteStore();
useEffect(() => {
if (input.length > 2) {
const timerId = setTimeout(() => {
fetchSuggestions(input);
}, 300);
return () => clearTimeout(timerId);
} else {
clearSuggestions();
}
}, [input, fetchSuggestions, clearSuggestions]);
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{isLoading && <div>Загрузка...</div>}
{!isLoading && suggestions && (
<div className="suggestions">{suggestions}</div>
)}
</div>
);
} |
|
Zustand имеет более плоскую кривую обучения по сравнению с Redux и меньше шаблонного кода.
React Query
React Query специализируется на управлении асинхронными запросами и кешированием данных, что идеально подходит для работы с ИИ-API.
| JavaScript | 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
| function AutocompleteInput() {
const [input, setInput] = useState('');
const debouncedInput = useDebounce(input, 300);
const { data: suggestions, isLoading } = useQuery(
['autocomplete', debouncedInput],
async () => {
if (debouncedInput.length < 3) return '';
const response = await fetch('/api/autocomplete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: debouncedInput })
});
if (!response.ok) throw new Error('Ошибка сети');
const data = await response.json();
return data.completion;
},
{
enabled: debouncedInput.length >= 3,
staleTime: 60000 // 1 минута
}
);
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{isLoading && <div>Загрузка...</div>}
{!isLoading && suggestions && (
<div className="suggestions">{suggestions}</div>
)}
</div>
);
}
// Хук для дебаунсинга
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timerId);
}, [value, delay]);
return debouncedValue;
} |
|
React Query особенно хорош для кэширования результатов запросов, что может значительно снизить количество запросов к API и улучшить отзывчивость интерфейса.
Клиентская vs. серверная обработка ИИ-запросов
Один из ключевых архитектурных вопросов – где именно обрабатывать запросы к ИИ-сервисам? У меня был проект, где мы перепробовали оба подхода, и я могу поделиться наблюдениями.
Клиентская обработка
Клиентская обработка предполагает, что все взаимодействие с ИИ-API происходит непосредственно из браузера. Технически это возможно – многие ИИ-сервисы предоставляют JavaScript SDK для работы с их API. Однако у такого подхода есть серьезные недостатки:
1. Безопасность API-ключей: Главная проблема – необходимость хранить API-ключи на клиенте, что практически гарантирует их утечку. Даже если вы используете переменные окружения в процесе сборки, ключи все равно будут доступны в финальном бандле.
2. Отсутствие контроля над расходами: Когда запросы идут напрямую от клиента, вы теряете возможность централизовано контролировать количество запросов и связанные с ними расходы.
3. Проблемы с CORS: Многие ИИ-API имеют ограничения по CORS, что может вызвать проблемы при прямых запросах из браузера.
4. Ограниченные возможности кэширования: В браузере сложнее реализовать эффективное кэширование результатов между разными пользователями.
Серверная обработка
Серверная обработка предполагает создание прокси-сервера, который будет принимать запросы от клиента, обогащать их API-ключами и перенаправлять к ИИ-сервису. Этот подход решает большинство проблем клиентской обработки:
1. Безопасность API-ключей: Ключи хранятся только на сервере и никогда не попадают к клиенту.
2. Контроль над расходами: Вы можете реализовать квотирование, лимитирование запросов и другие механизмы контроля.
3. Отсутствие проблем с CORS: Ваш сервер обходит ограничения CORS, делая запросы от имени клиента.
4. Эффективное кэширование: На сервере можно реализовать кэширование результатов, которое будет работать для всех пользователей.
Однако серверная обработка тоже имеет недостатки:
1. Дополнительная задержка: Запросы проходят дополнительное звено, что увеличивает время ответа.
2. Необходимость поддерживать бэкенд: Даже для чисто фронтенд-проектов приходится разворачивать и поддерживать бэкендсервер.
3. Масштабирование: При большом количестве запросов прокси-сервер может стать узким местом.
На практике я почти всегда выбираю серверную обработку, несмотря на ее недостатки. Безопасность и контроль над расходами перевешивают дополнительную сложность и задержку.
Оптимизация производительности ИИ-автозаполнения
Один из ключевых вызовов при работе с ИИ-автозаполнением – это обеспечение отзывчивости интерфейса. Запросы к ИИ-моделям могут занимать сотни миллисекунд, что крайне много для интерактивного интерфейса.
Дебаунсинг и тротлинг
Первое, что нужно реализовать – это дебаунсинг ввода пользователя. Мы не хотим отправлять запрос на каждое нажатие клавиши, особенно учитывая, что большинство ИИ-API имеют ограничения на количество запросов в минуту.
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timerId);
}, [value, delay]);
return debouncedValue;
} |
|
В некоторых случаях тротлинг может быть более подходящим вариантом, особенно если вы хотите обеспечить регулярное обновление предложений при продолжительном вводе:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function useThrottle(value, limit) {
const [throttledValue, setThrottledValue] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value);
lastRan.current = Date.now();
}
}, limit - (Date.now() - lastRan.current));
return () => clearTimeout(handler);
}, [value, limit]);
return throttledValue;
} |
|
Предварительная загрузка и кэширование
Другой способ повысить отзывчивость – предварительная загрузка вероятных продолжений. Например, можно отправить запрос к ИИ не только для текущего ввода, но и для нескольких вариантов его возможного продолжения.
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| function usePredictiveAutocomplete(input) {
// Текущие предложения
const { data: currentSuggestions } = useQuery(
['autocomplete', input],
() => fetchSuggestions(input)
);
// Предварительная загрузка для вероятных продолжений
useEffect(() => {
if (currentSuggestions && currentSuggestions.length > 0) {
// Предполагаем, что пользователь выберет одно из текущих предложений
currentSuggestions.forEach(suggestion => {
const nextInput = input + ' ' + suggestion;
// Предзагружаем следующие предложения
queryClient.prefetchQuery(
['autocomplete', nextInput],
() => fetchSuggestions(nextInput)
);
});
}
}, [input, currentSuggestions, queryClient]);
return { suggestions: currentSuggestions };
} |
|
Этот подход может значительно ускорить отзывчивость интерфейса, особенно в случаях, когда пользователь следует типичным паттернам ввода.
Управление сложными последовательностями взаимодействия
Иногда автозаполнение - это не просто одиночный запрос, а сложная последовательность взаимодействия с ИИ. Например, вы можете хотеть подстраивать предложения под предыдущие выборы пользователя или учитывать более широкий контекст.
Для таких случаев полезно использовать машины состояний или паттерн "Цепочка ответственности" (Chain of Responsibility). Я часто использую библиотеку XState для моделирования сложных взаимодействий:
| JavaScript | 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
| import { createMachine, interpret } from 'xstate';
const autocompleteMachine = createMachine({
id: 'autocomplete',
initial: 'idle',
context: {
input: '',
suggestions: [],
history: [] // История выбранных предложений
},
states: {
idle: {
on: {
TYPE: {
target: 'loading',
actions: 'updateInput'
}
}
},
loading: {
invoke: {
src: 'fetchSuggestions',
onDone: {
target: 'success',
actions: 'setSuggestions'
},
onError: {
target: 'error',
actions: 'setError'
}
}
},
success: {
on: {
TYPE: {
target: 'loading',
actions: 'updateInput'
},
SELECT: {
target: 'idle',
actions: ['updateInput', 'addToHistory']
}
}
},
error: {
on: {
TYPE: {
target: 'loading',
actions: 'updateInput'
}
}
}
}
}); |
|
Такой подход позволяет моделировать сложные взаимодействия и четко отделять логику состояний от логики компонентов.
Реализация с OpenAI API
Теперь, когда мы определились с архитектурными подходами, давайте засучим рукава и погрузимся в практическую реализацию. Я расскажу, как настроить взаимодействие React-приложения с OpenAI API, разберу примеры кода и поделюсь своим реальным опытом реализации ИИ-автозаполнения.
Настройка подключения и базовая обработка запросов
Для начала нам нужно настроить прокси-сервер, который будет обрабатывать запросы от фронтенда и взаимодействовать с OpenAI. Вот пример настройки Express-сервера:
| JavaScript | 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
| // server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { OpenAI } = require('openai');
const app = express();
app.use(cors());
app.use(express.json());
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY // Никогда не хардкодьте ключи!
});
app.post('/api/autocomplete', async (req, res) => {
const { prompt } = req.body;
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: [INLINE]Complete this: ${prompt}[/INLINE] }],
temperature: 0.7,
max_tokens: 20
});
res.json({
completion: completion.choices[0].message.content.trim()
});
} catch (err) {
console.error('API Error:', err);
// Обработка разных типов ошибок
if (err.response) {
res.status(err.response.status).json({
error: err.response.data.error.message
});
} else {
res.status(500).json({
error: 'Что-то пошло не так'
});
}
}
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Сервер запущен на порту ${PORT}`)); |
|
На стороне React нам нужен компонент, который будет отправлять запросы и отображать результаты:
| JavaScript | 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
| // AutocompleteInput.jsx
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import './AutocompleteInput.css';
function AutocompleteInput() {
const [input, setInput] = useState('');
const [suggestion, setSuggestion] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const inputRef = useRef(null);
useEffect(() => {
if (input.length < 3) {
setSuggestion('');
return;
}
const fetchSuggestion = async () => {
setIsLoading(true);
try {
const res = await axios.post('/api/autocomplete', { prompt: input });
setSuggestion(res.data.completion);
} catch (err) {
console.error('Ошибка при получении подсказки:', err);
setError('Не удалось загрузить подсказки');
} finally {
setIsLoading(false);
}
};
// Дебаунсинг запросов
const timerId = setTimeout(fetchSuggestion, 300);
return () => clearTimeout(timerId);
}, [input]);
const handleInputChange = (e) => {
setInput(e.target.value);
setError(null);
};
// Применение подсказки по нажатию Tab
const handleKeyDown = (e) => {
if (e.key === 'Tab' && suggestion) {
e.preventDefault();
setInput(input + suggestion);
setSuggestion('');
}
};
return (
<div className="autocomplete-container">
<input
ref={inputRef}
type="text"
value={input}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Начните вводить..."
className="autocomplete-input"
/>
{isLoading && <div className="loading-indicator">Загрузка...</div>}
{error && <div className="error-message">{error}</div>}
{suggestion && !isLoading && (
<div className="suggestion">
<span className="input-text">{input}</span>
<span className="suggestion-text">{suggestion}</span>
<small>Нажмите Tab для завершения</small>
</div>
)}
</div>
);
}
export default AutocompleteInput; |
|
В этой базовой реализации есть несколько важных моментов:
1. Дебаунсинг запросов (не отправлять запрос на каждое нажатие клавиши).
2. Обработка состояний загрузки и ошибок.
3. Возможность принять подсказку по нажатию Tab.
4. Минимальная длина ввода перед запросом (3 символа).
Настройка промптов для разных доменов применения
Одна из ключевых вещей, которую я понял на опыте - качество автозаполнения сильно зависит от того, как вы формулируете промпт. Разные типы контента требуют разных подходов. Вот несколько примеров специализированных промптов:
Для деловой переписки:
| JavaScript | 1
2
| const emailPrompt = `Я пишу деловое письмо коллеге. Вот начало: "${userInput}"
Предложи продолжение фразы (не более 20 слов) в формальном деловом стиле.`; |
|
Для программного кода:
| JavaScript | 1
2
3
4
5
| const codePrompt = `Дополни код функции на JavaScript:
\`\`\`javascript
${userInput}
\`\`\`
Предложи только следующую строку или блок кода.`; |
|
Для творческих текстов:
| JavaScript | 1
2
| const creativePrompt = `Я пишу художественный текст. Вот начало абзаца: "${userInput}"
Продолжи эту мысль в том же стиле (не более 20 слов).`; |
|
Улучшить промпты можно путем включения контекста, примеров и ограничений. Я часто провожу A/B-тестирование разных формулировок промптов, чтобы выбрать наиболее эффективную. Вот пример функции, которая выбирает промпт в зависимости от контекста:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function getContextAwarePrompt(input, context) {
switch(context) {
case 'code':
return `Дополни код: \`${input}\``;
case 'email':
return `Я пишу email. Вот начало: "${input}". Предложи продолжение.`;
case 'chat':
return `В чате с другом я написал: "${input}". Как можно продолжить?`;
default:
return `Complete this: ${input}`;
}
}
// Использование
app.post('/api/autocomplete', async (req, res) => {
const { prompt, context } = req.body;
const contextualPrompt = getContextAwarePrompt(prompt, context);
// ... отправка запроса к OpenAI с контекстуальным промптом
}); |
|
Стратегии обработки потокового ответа
Для создания максимально отзывчивого интерфейса лучше использовать потоковые ответы от API OpenAI. Это позволит показывать предложения по мере их генерации, а не ждать полного ответа.
На серверной стороне:
| JavaScript | 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
| app.post('/api/autocomplete/stream', async (req, res) => {
const { prompt } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: [INLINE]Complete this: ${prompt}[/INLINE] }],
temperature: 0.7,
max_tokens: 20,
stream: true
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (err) {
console.error('Error streaming:', err);
res.write(`data: ${JSON.stringify({ error: 'Ошибка потоковой передачи' })}\n\n`);
res.end();
}
}); |
|
А на клиентской стороне:
| JavaScript | 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
| function StreamingAutocomplete() {
const [input, setInput] = useState('');
const [streamingSuggestion, setStreamingSuggestion] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
if (input.length < 3) {
setStreamingSuggestion('');
return;
}
setIsStreaming(true);
let suggestion = '';
const eventSource = new EventSource(`/api/autocomplete/stream?prompt=${encodeURIComponent(input)}`);
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
setIsStreaming(false);
eventSource.close();
return;
}
const data = JSON.parse(event.data);
suggestion += data.content;
setStreamingSuggestion(suggestion);
};
eventSource.onerror = () => {
setIsStreaming(false);
eventSource.close();
};
return () => {
eventSource.close();
};
}, [input]);
// ... остальной код компонента
} |
|
Это создаст эффект "печатающегося" текста, что делает интерфейс более живым и интерактивным.
Батчинг запросов и управление лимитами API
Работа с OpenAI API требует внимательного отношения к rate limits (ограничениям частоты запросов). Для высоконагруженных приложений полезно реализовать батчинг запросов и очередь. Вот пример простой реализации очереди запросов:
| JavaScript | 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
| class RequestQueue {
constructor() {
this.queue = [];
this.processing = false;
this.rateLimit = 10; // запросов в минуту
this.timeWindow = 60000; // миллисекунд
this.requestTimes = [];
}
enqueue(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
// Проверка rate limit
const now = Date.now();
this.requestTimes = this.requestTimes.filter(time => now - time < this.timeWindow);
if (this.requestTimes.length >= this.rateLimit) {
// Дождемся освобождения слота
const oldestRequest = this.requestTimes[0];
const waitTime = this.timeWindow - (now - oldestRequest);
setTimeout(() => {
this.processing = false;
this.processQueue();
}, waitTime + 100);
return;
}
// Обработка запроса
const { request, resolve, reject } = this.queue.shift();
this.requestTimes.push(now);
try {
const result = await request();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.processing = false;
this.processQueue();
}
}
}
// Использование
const requestQueue = new RequestQueue();
app.post('/api/autocomplete', async (req, res) => {
try {
const result = await requestQueue.enqueue(() => {
return openai.chat.completions.create({
// параметры запроса
});
});
res.json({ completion: result.choices[0].message.content });
} catch (err) {
res.status(500).json({ error: 'Ошибка при выполнении запроса' });
}
}); |
|
Такой подход позволяет контролировать частоту запросов и избегать ошибок превышения лимитов API.
Персонализация автозаполнения
Чтобы сделать автозаполнение более релевантным для конкретного пользователя, можно использовать анализ его предыдущих взаимодействий. Например, можно сохранять историю выбранных пользователем подсказок и использовать ее для улучшения будущих предложений.
| JavaScript | 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
| function PersonalizedAutocomplete() {
const [input, setInput] = useState('');
const [suggestion, setSuggestion] = useState('');
const [history, setHistory] = useState([]);
// Получение персонализированной подсказки
const fetchPersonalizedSuggestion = async () => {
try {
const response = await axios.post('/api/personalized-autocomplete', {
prompt: input,
history: history.slice(-5) // Последние 5 выбранных подсказок
});
setSuggestion(response.data.completion);
} catch (error) {
console.error('Ошибка при получении персонализированной подсказки:', error);
}
};
// При выборе подсказки добавляем ее в историю
const acceptSuggestion = () => {
if (suggestion) {
const newInput = input + suggestion;
setInput(newInput);
setSuggestion('');
setHistory(prev => [...prev, newInput]);
}
};
// ... остальной код компонента
} |
|
На серверной стороне можно учитывать историю в промпте:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| app.post('/api/personalized-autocomplete', async (req, res) => {
const { prompt, history } = req.body;
// Формируем промпт с учетом истории
const historyContext = history.length > 0
? `Based on the user's previous messages: ${history.join(' | ')}\n`
: '';
const personalizedPrompt = `${historyContext}Complete this message: ${prompt}`;
// ... запрос к OpenAI с персонализированным промптом
}); |
|
Углубленная персонализация на основе пользовательских данных
Когда я впервые начал работать с персонализацией автозаполнения, то быстро понял, что простого сохранения истории запросов недостаточно. Настоящая персонализация требует комплексного подхода к анализу пользовательского поведения.
Вот продвинутый пример персонализации, который я реализовал в одном из проектов. Мы создали систему, которая анализировала не только историю запросов, но и время между нажатиями клавиш, частоту использования определенных слов и даже время суток, когда пользователь наиболее активен.
| JavaScript | 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
| // Класс для анализа пользовательского поведения
class UserBehaviorAnalyzer {
constructor(userId) {
this.userId = userId;
this.keyPressTimings = [];
this.wordFrequency = {};
this.acceptedSuggestions = [];
this.rejectedSuggestions = [];
this.activeHours = Array(24).fill(0);
}
recordKeyPress(timestamp) {
this.keyPressTimings.push(timestamp);
// Анализируем время суток
const hour = new Date(timestamp).getHours();
this.activeHours[hour]++;
// Очищаем старые записи (старше недели)
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
this.keyPressTimings = this.keyPressTimings.filter(time => time > weekAgo);
}
recordWordUsage(word) {
this.wordFrequency[word] = (this.wordFrequency[word] || 0) + 1;
}
recordSuggestionResponse(suggestion, accepted) {
if (accepted) {
this.acceptedSuggestions.push({suggestion, timestamp: Date.now()});
} else {
this.rejectedSuggestions.push({suggestion, timestamp: Date.now()});
}
}
// Получаем профиль пользователя для персонализации
getUserProfile() {
// Анализируем скорость печати
let typingSpeed = 'average';
if (this.keyPressTimings.length > 10) {
const intervals = [];
for (let i = 1; i < this.keyPressTimings.length; i++) {
intervals.push(this.keyPressTimings[i] - this.keyPressTimings[i-1]);
}
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
if (avgInterval < 150) typingSpeed = 'fast';
else if (avgInterval > 300) typingSpeed = 'slow';
}
// Находим наиболее часто используемые слова
const commonWords = Object.entries(this.wordFrequency)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(entry => entry[0]);
// Определяем, когда пользователь наиболее активен
const mostActiveHour = this.activeHours.indexOf(Math.max(...this.activeHours));
// Анализируем, какие типы предложений принимаются чаще
const acceptanceRate = this.acceptedSuggestions.length /
(this.acceptedSuggestions.length + this.rejectedSuggestions.length || 1);
return {
typingSpeed,
commonWords,
mostActiveHour,
acceptanceRate
};
}
} |
|
Этот анализатор поведения может быть интегрирован с компонентом автозаполнения:
| JavaScript | 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
| function EnhancedAutocomplete({ userId }) {
const [input, setInput] = useState('');
const [suggestion, setSuggestion] = useState('');
const behaviorAnalyzer = useMemo(() => new UserBehaviorAnalyzer(userId), [userId]);
const handleKeyPress = useCallback((e) => {
behaviorAnalyzer.recordKeyPress(Date.now());
// Анализируем вводимые слова
if (e.key === ' ' || e.key === '.' || e.key === ',') {
const lastWord = input.split(/\s+/).pop();
if (lastWord) behaviorAnalyzer.recordWordUsage(lastWord);
}
}, [behaviorAnalyzer, input]);
const fetchPersonalizedSuggestion = useCallback(async () => {
const userProfile = behaviorAnalyzer.getUserProfile();
try {
const response = await axios.post('/api/personalized-autocomplete', {
prompt: input,
userProfile
});
setSuggestion(response.data.completion);
} catch (error) {
console.error('Ошибка при получении персонализированной подсказки:', error);
}
}, [input, behaviorAnalyzer]);
// Используем запрос при каждом изменении ввода
useEffect(() => {
if (input.length < 3) return;
const timerId = setTimeout(fetchPersonalizedSuggestion, 300);
return () => clearTimeout(timerId);
}, [input, fetchPersonalizedSuggestion]);
// ... остальной код компонента
} |
|
На сервере мы можем использовать этот профиль для создания более персонализированных промптов:
| JavaScript | 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
| app.post('/api/personalized-autocomplete', async (req, res) => {
const { prompt, userProfile } = req.body;
// Создаем персонализированный промпт
let personalizedPrompt = `Complete this message: ${prompt}`;
if (userProfile) {
personalizedPrompt = `The user typically types ${userProfile.typingSpeed} and commonly uses words like ${userProfile.commonWords.join(', ')}. They are currently active during what is typically their ${userProfile.mostActiveHour}th hour of activity. They tend to ${userProfile.acceptanceRate > 0.7 ? 'accept' : 'be selective about'} suggestions.
Complete this message in a way that matches their style: ${prompt}`;
}
// Запрос к OpenAI
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: personalizedPrompt }],
temperature: 0.7,
max_tokens: 20
});
res.json({ completion: completion.choices[0].message.content.trim() });
} catch (err) {
res.status(500).json({ error: 'Ошибка при обработке запроса' });
}
}); |
|
Fine-tuning моделей для конкретных доменов
Если ваше приложение работает в специфической области (юриспруденция, медицина, программирование), то для получения по-настоящему качественных результатов стоит рассмотреть fine-tuning (дообучение) модели. Это особенно полезно, когда у вас есть собственный набор данных с примерами предпочтительных завершений. Fine-tuning требует подготовки специального датасета с парами "запрос-ответ". Например:
| JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [
{
"messages": [
{"role": "system", "content": "Вы помогаете завершить юридический документ."},
{"role": "user", "content": "Настоящим соглашением стороны договариваются о"}
]
},
{
"messages": [
{"role": "system", "content": "Вы помогаете завершить юридический документ."},
{"role": "user", "content": "В случае нарушения условий контракта"}
]
}
] |
|
Пример кода для запуска fine-tuning с использованием API OpenAI:
| JavaScript | 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
| async function startFineTuning() {
try {
// Загрузить файл тренировочных данных
const file = await openai.files.create({
file: fs.createReadableStream('training_data.jsonl'),
purpose: 'fine-tune'
});
// Запустить процесс fine-tuning
const fineTune = await openai.fineTuning.jobs.create({
training_file: file.id,
model: 'gpt-3.5-turbo',
suffix: 'legal-autocomplete'
});
console.log('Fine-tuning job started:', fineTune);
// Функция для проверки статуса
async function checkStatus() {
const status = await openai.fineTuning.jobs.retrieve(fineTune.id);
console.log('Current status:', status.status);
if (status.status === 'succeeded') {
console.log('Fine-tuning completed! Model ID:', status.fine_tuned_model);
} else if (status.status === 'failed') {
console.error('Fine-tuning failed:', status.error);
} else {
// Продолжаем проверять
setTimeout(checkStatus, 60000); // проверка раз в минуту
}
}
// Начать проверку статуса
setTimeout(checkStatus, 60000);
} catch (error) {
console.error('Error starting fine-tuning:', error);
}
}
// Запустить процесс
startFineTuning(); |
|
После fine-tuning вы можете использовать полученную модель для автозаполнения:
| JavaScript | 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
| app.post('/api/domain-specific-autocomplete', async (req, res) => {
const { prompt, domain } = req.body;
// Выбор модели в зависимости от домена
let modelId = 'gpt-4'; // модель по умолчанию
if (domain === 'legal') {
modelId = 'ft:gpt-3.5-turbo:my-org:legal-autocomplete:id';
} else if (domain === 'medical') {
modelId = 'ft:gpt-3.5-turbo:my-org:medical-autocomplete:id';
}
try {
const completion = await openai.chat.completions.create({
model: modelId,
messages: [{ role: 'user', content: [INLINE]Complete this: ${prompt}[/INLINE] }],
temperature: 0.3, // Меньшая температура для более предсказуемых ответов
max_tokens: 30
});
res.json({ completion: completion.choices[0].message.content.trim() });
} catch (err) {
res.status(500).json({ error: 'Ошибка при обработке запроса' });
}
}); |
|
Расширенная обработка ошибок и отказоустойчивость
В реальных приложениях крайне важно обеспечить отказоустойчивость системы автозаполнения. Я сталкивался с ситуациями, когда OpenAI API становился недоступен или отвечал с большой задержкой, и в таких случаях важно иметь запасной план.
Вот пример реализации отказоустойчивой системы с переходом на резервный механизм автозаполнения:
| JavaScript | 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
| async function getReliableAutocomplete(prompt, options = {}) {
const { timeout = 3000, retries = 2, fallbackMode = 'local' } = options;
// Функция для запроса к OpenAI с таймаутом
const fetchWithTimeout = async () => {
return new Promise(async (resolve, reject) => {
// Устанавливаем таймаут
const timeoutId = setTimeout(() => {
reject(new Error('OpenAI API timeout'));
}, timeout);
try {
const result = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: [INLINE]Complete this: ${prompt}[/INLINE] }],
temperature: 0.7,
max_tokens: 20
});
clearTimeout(timeoutId);
resolve(result.choices[0].message.content.trim());
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
});
};
// Попытка с ретраями
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fetchWithTimeout();
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed:`, error);
if (attempt === retries) {
console.error('All OpenAI API attempts failed, falling back to alternative');
break;
}
// Экспоненциальное отступление
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
}
}
// Запасной вариант, если OpenAI недоступен
if (fallbackMode === 'local') {
return getLocalAutocomplete(prompt);
} else if (fallbackMode === 'cached') {
return getCachedAutocomplete(prompt);
} else {
// Если всё провалилось, возвращаем null или пустую строку
return '';
}
}
// Простая реализация локального автозаполнения
function getLocalAutocomplete(prompt) {
// Это может быть простая статистическая модель или предопределенные фразы
const lastWord = prompt.split(/\s+/).pop().toLowerCase();
const commonCompletions = {
'привет': ' всем!',
'спасибо': ' за внимание',
'пожалуйста': ' обратите внимание',
// ... другие распространенные завершения
};
return commonCompletions[lastWord] || '';
}
// Получение из кэша похожих запросов
function getCachedAutocomplete(prompt) {
// Здесь можно реализовать поиск похожих запросов в кэше
// и вернуть соответствующие завершения
return '';
} |
|
Этот подход обеспечивает надежность системы автозаполнения даже при проблемах с API OpenAI. В моей практике такая система спасала не раз, особенно в периоды высокой нагрузки на API или при нестабильном интернет-соединении.
Оптимизация пользовательского интерфейса
Одна вещь, которую я твердо усвоил за годы разработки, — даже самая мощная ИИ-модель бесполезна, если пользовательский интерфейс тормозит и раздражает. Дьявол, как всегда, кроется в деталях реализации UI. Давайте разберемся, как сделать интерфейс автозаполнения по-настоящему отзывчивым и приятным в использовании.
Дебаунсинг, кеширование и адаптивная загрузка контента
Мы уже кратко упоминали дебаунсинг в предыдущих разделах, но он настолько критичен для оптимизации UI, что стоит остановиться на нем подробнее. Представьте ситуацию: пользователь быстро печатает, а ваше приложение отправляет запрос к API на каждое нажатие клавиши. Это приведет к "гонке запросов", когда более поздние ответы могут приходить раньше ранних, создавая хаос в интерфейсе. Не говоря уже о том, что вы быстро исчерпаете квоту API и разорите своего босса на счетах от OpenAI.
Вот мой любимый хук для дебаунсинга, который я использую во всех проектах:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Идентификатор таймера для очистки
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Очищаем таймер при изменении value или unmount
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
} |
|
Использование этого хука позволяет отправлять запросы только когда пользователь сделал паузу во вводе:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| function AutocompleteInput() {
const [input, setInput] = useState('');
const debouncedInput = useDebounce(input, 400); // 400ms задержка
// Запрос отправляется только когда debouncedInput изменится
useEffect(() => {
if (debouncedInput.length < 3) return;
fetchSuggestions(debouncedInput);
}, [debouncedInput]);
// ...остальной код компонента
} |
|
Но дебаунсинга недостаточно. Я видел много приложений, которые отправляют один и тот же запрос снова и снова, просто потому что пользователь удалил символ и потом снова его добавил. Здесь на помощь приходит кэширование.
Простейшее кэширование можно реализовать с помощью простого объекта:
| JavaScript | 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
| function AutocompleteWithCache() {
const [input, setInput] = useState('');
const debouncedInput = useDebounce(input, 400);
const [suggestions, setSuggestions] = useState('');
const [isLoading, setIsLoading] = useState(false);
const cacheRef = useRef({});
useEffect(() => {
if (debouncedInput.length < 3) {
setSuggestions('');
return;
}
// Проверяем кэш перед запросом
if (cacheRef.current[debouncedInput]) {
setSuggestions(cacheRef.current[debouncedInput]);
return;
}
setIsLoading(true);
fetchSuggestions(debouncedInput)
.then(result => {
// Сохраняем в кэше
cacheRef.current[debouncedInput] = result;
setSuggestions(result);
})
.catch(error => console.error('Ошибка получения подсказок:', error))
.finally(() => setIsLoading(false));
}, [debouncedInput]);
// ...остальной код компонента
} |
|
Для более сложных случаев я рекомендую использовать React Query, который предоставляет мощные возможности кэширования с контролем времени жизни кэша, инвалидацией и префетчингом.
Еще один важный аспект оптимизации — адаптивная загрузка. Не всегда нужно показывать спиннер при каждом запросе. Если запрос выполняется быстро (менее 300 мс), показ и скрытие индикатора загрузки только отвлекает пользователя. Вот как я обычно решаю эту проблему:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function AdaptiveLoadingIndicator({ isLoading }) {
const [showSpinner, setShowSpinner] = useState(false);
useEffect(() => {
let timerId;
if (isLoading) {
// Показываем спиннер только если загрузка занимает более 300 мс
timerId = setTimeout(() => setShowSpinner(true), 300);
} else {
setShowSpinner(false);
}
return () => clearTimeout(timerId);
}, [isLoading]);
if (!showSpinner) return null;
return <div className="spinner">Загрузка...</div>;
} |
|
Реализация офлайн-режима с локальным кешированием
Одно из самых неприятных ощущений для пользователя — когда функция автозаполнения внезапно перестает работать из-за проблем с сетью. Реализация офлайн-режима может значительно улучшить это впечатление.
Моя стратегия включает:
1. Использование localStorage для хранения кэша.
2. Отслеживание состояния сети.
3. Применение стратегии "сначала кэш, потом сеть" для плавной деградации.
Вот пример компонента с офлайн-режимом:
| JavaScript | 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
| function OfflineAwareAutocomplete() {
const [input, setInput] = useState('');
const [suggestions, setSuggestions] = useState('');
const [isOnline, setIsOnline] = useState(navigator.onLine);
const debouncedInput = useDebounce(input, 400);
// Отслеживаем состояние сети
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// Загрузка предложений с учетом офлайн-режима
useEffect(() => {
if (debouncedInput.length < 3) {
setSuggestions('');
return;
}
// Сначала проверяем локальный кэш
const cachedSuggestions = localStorage.getItem(`autocomplete_${debouncedInput}`);
if (cachedSuggestions) {
setSuggestions(JSON.parse(cachedSuggestions));
}
// Если онлайн, запрашиваем новые данные
if (isOnline) {
fetchSuggestions(debouncedInput)
.then(result => {
setSuggestions(result);
// Обновляем кэш
localStorage.setItem(`autocomplete_${debouncedInput}`, JSON.stringify(result));
})
.catch(error => {
console.error('Ошибка получения подсказок:', error);
// В случае ошибки оставляем данные из кэша
});
} else {
// Если офлайн и нет кэша, показываем сообщение
if (!cachedSuggestions) {
setSuggestions(
'Нет интернет-соединения. Попробуйте ввести другой текст или подключитесь к интернету.'
);
}
}
}, [debouncedInput, isOnline]);
// ...остальной код компонента
} |
|
Для более продвинутых сценариев я рекомендую рассмотреть IndexedDB вместо localStorage — она поддерживает большие объемы данных и асинхронные операции.
Еще один трюк, который я часто использую — это создание локальной версии API автозаполнения, которая работает полностью на клиенте:
| JavaScript | 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
| // Простой алгоритм для подбора подсказок на основе частотного анализа
function getLocalSuggestions(input) {
// Эти данные можно загрузить при инициализации приложения
const commonPhrases = {
'привет': ['всем', 'как дела', 'мир'],
'как': ['дела', 'вы', 'это работает'],
'спасибо': ['за внимание', 'большое', 'за помощь'],
// и так далее...
};
const words = input.toLowerCase().split(/\s+/);
const lastWord = words[words.length - 1];
// Если последнее слово есть в нашем словаре, предлагаем продолжения
if (commonPhrases[lastWord]) {
return commonPhrases[lastWord][0]; // берем самый популярный вариант
}
// Простая эвристика для поиска частичных совпадений
for (const [key, suggestions] of Object.entries(commonPhrases)) {
if (key.startsWith(lastWord) && lastWord.length > 2) {
return key.slice(lastWord.length) + ' ' + suggestions[0];
}
}
return '';
} |
|
Этот примитивный алгоритм можно значительно улучшить, используя n-граммы или даже небольшие модели машинного обучения, которые могут работать в браузере (например, с помощью TensorFlow.js).
Создание адаптивных компонентов автозаполнения для мобильных устройств
Когда я впервые запустил свое приложение с ИИ-автозаполнением на телефоне, результат был... скажем так, не впечатляющим. Экранная клавиатура конфликтовала с моим UI, предложения были слишком длинными и занимали весь экран, а время загрузки на мобильной сети превращало всю "магию" в раздражающее ожидание.
Вот мои главные принципы для мобильного UI с автозаполнением:
1. Компактность без потери информативности. Предложения должны быть короткими и полезными.
2. Учет экранной клавиатуры. UI не должен перекрываться клавиатурой.
3. Экономия трафика. Запросы должны быть оптимизированы для медленных соединений.
Вот пример компонента, адаптированного для мобильных устройств:
| JavaScript | 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
| function MobileAwareAutocomplete() {
const [input, setInput] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const debouncedInput = useDebounce(input, isMobile ? 600 : 400); // Увеличиваем задержку на мобильных
// Определяем, мобильное устройство или нет
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Запрос предложений с учетом типа устройства
useEffect(() => {
if (debouncedInput.length < 3) {
setSuggestions([]);
return;
}
setIsLoading(true);
// На мобильных запрашиваем меньше токенов
const maxTokens = isMobile ? 10 : 20;
fetchSuggestions(debouncedInput, maxTokens)
.then(results => {
// На мобильных показываем меньше вариантов
const limitedResults = isMobile ? results.slice(0, 2) : results;
setSuggestions(limitedResults);
})
.catch(error => console.error('Ошибка:', error))
.finally(() => setIsLoading(false));
}, [debouncedInput, isMobile]);
return (
<div className={`autocomplete-container ${isMobile ? 'mobile' : ''}`}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Введите текст..."
/>
{isLoading && <div className="loading-indicator">...</div>}
{!isLoading && suggestions.length > 0 && (
<div className={`suggestions-container ${isMobile ? 'mobile-suggestions' : ''}`}>
{suggestions.map((suggestion, index) => (
<div
key={index}
className="suggestion-item"
onClick={() => {
setInput(input + ' ' + suggestion);
setSuggestions([]);
}}
>
{suggestion}
</div>
))}
</div>
)}
</div>
);
} |
|
А вот соответствующие стили для мобильной версии:
| CSS | 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
| .autocomplete-container {
position: relative;
width: 100%;
max-width: 600px;
}
.autocomplete-container.mobile {
max-width: 100%;
}
.suggestions-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 10;
}
.mobile-suggestions {
position: fixed;
bottom: 0;
top: auto;
left: 0;
right: 0;
max-height: 30vh;
border: none;
border-top: 1px solid #ddd;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.suggestion-item {
padding: 10px;
cursor: pointer;
}
.suggestion-item:hover {
background: #f5f5f5;
}
.mobile .suggestion-item {
padding: 15px; /* Увеличенная область нажатия для тачскрина */
} |
|
Одним из ключевых аспектов мобильной оптимизации является учет различных сценариев взаимодействия. На десктопе пользователи обычно нажимают Tab для принятия предложения, а на мобильных устройствах нужно предоставить крупные, удобные для касания элементы.
Умные UX-паттерны для автозаполнения
После того, как я несколько раз переписал UI автозаполнения для разных проектов, я выработал несколько UX-паттернов, которые делают взаимодействие с ИИ-подсказками более естественным и приятным.
Во-первых, это "гибридное автозаполнение" — комбинация встроенных подсказок и выпадающего списка:
| JavaScript | 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
| function HybridAutocomplete() {
const [input, setInput] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [inlineSuggestion, setInlineSuggestion] = useState('');
const inputRef = useRef(null);
// Получаем встроенную подсказку и список вариантов
useEffect(() => {
if (input.length < 3) {
setInlineSuggestion('');
setSuggestions([]);
return;
}
// Для встроенной подсказки берем лучший вариант
fetchBestSuggestion(input)
.then(best => setInlineSuggestion(best))
.catch(e => console.error(e));
// Для выпадающего списка получаем варианты
fetchAlternativeSuggestions(input)
.then(alternatives => setSuggestions(alternatives))
.catch(e => console.error(e));
}, [input]);
// Обработка принятия встроенной подсказки по Tab
const handleKeyDown = (e) => {
if (e.key === 'Tab' && inlineSuggestion) {
e.preventDefault();
setInput(input + inlineSuggestion);
setInlineSuggestion('');
}
};
return (
<div className="hybrid-autocomplete">
<div className="input-wrapper">
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
{inlineSuggestion && (
<div className="inline-suggestion">
<span className="entered-text">{input}</span>
<span className="suggestion-text">{inlineSuggestion}</span>
</div>
)}
</div>
{suggestions.length > 0 && (
<ul className="suggestions-list">
{suggestions.map((suggestion, index) => (
<li
key={index}
onClick={() => {
setInput(input + ' ' + suggestion);
setSuggestions([]);
}}
>
{suggestion}
</li>
))}
</ul>
)}
</div>
);
} |
|
Я обнаружил, что такой подход дает пользователям больше контроля и снижает когнитивную нагрузку — можно быстро принять наиболее вероятное предложение по Tab или выбрать альтернативу из списка.
Доступность (a11y) компонентов автозаполнения
Доступность часто упускают из виду, но она критически важна для многих пользователей. Я в свое время получил довольно болезненный фидбек от сообщества, когда проигнорировал a11y в компоненте автозаполнения, который мы внедрили в популярный опен-сорс проект. Вот как можно улучшить доступность компонента автозаполнения:
| JavaScript | 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
| function AccessibleAutocomplete() {
const [input, setInput] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [activeIndex, setActiveIndex] = useState(-1);
const [isOpen, setIsOpen] = useState(false);
const comboboxId = useId();
const listboxId = `${comboboxId}-listbox`;
// Обработка клавиатурной навигации
const handleKeyDown = (e) => {
if (!isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev => Math.min(prev + 1, suggestions.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
if (activeIndex >= 0) {
e.preventDefault();
selectSuggestion(suggestions[activeIndex]);
}
break;
case 'Escape':
e.preventDefault();
setIsOpen(false);
break;
default:
break;
}
};
const selectSuggestion = (suggestion) => {
setInput(input + ' ' + suggestion);
setSuggestions([]);
setIsOpen(false);
setActiveIndex(-1);
};
return (
<div className="accessible-autocomplete">
<label htmlFor={comboboxId}>Поиск:</label>
<input
id={comboboxId}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
aria-autocomplete="list"
aria-controls={isOpen ? listboxId : undefined}
aria-activedescendant={
activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined
}
role="combobox"
aria-expanded={isOpen}
/>
{isOpen && suggestions.length > 0 && (
<ul
id={listboxId}
role="listbox"
className="suggestions-list"
>
{suggestions.map((suggestion, index) => (
<li
id={`${listboxId}-option-${index}`}
key={index}
role="option"
aria-selected={index === activeIndex}
onClick={() => selectSuggestion(suggestion)}
className={index === activeIndex ? 'active' : ''}
>
{suggestion}
</li>
))}
</ul>
)}
</div>
);
} |
|
Эти атрибуты ARIA делают компонент понятным для скринридеров и других вспомогательных технологий. Кроме того, мы обеспечиваем полную клавиатурную навигацию.
Визуальная обратная связь и анимации
Одна из фишек, которая резко повышает воспринимаемое качество автозаполнения, — это правильная визуальная обратная связь. Пользователь должен четко понимать, что происходит, даже если запрос занимает время.
Я люблю использовать плавные, ненавязчивые анимации, которые создают ощущение "живого" интерфейса:
| CSS | 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
| .suggestion-appear {
opacity: 0;
transform: translateY(-10px);
}
.suggestion-appear-active {
opacity: 1;
transform: translateY(0);
transition: opacity 200ms, transform 200ms;
}
.typing-indicator {
display: inline-block;
}
.typing-indicator span {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #888;
margin: 0 2px;
animation: typing 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 80%, 100% { transform: scale(0.6); }
40% { transform: scale(1); }
} |
|
А вот и соответствующий компонент индикатора набора текста:
| JavaScript | 1
2
3
4
5
6
7
8
9
| function TypingIndicator() {
return (
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
);
} |
|
Такой индикатор создает ощущение, что ИИ "думает" над ответом, что психологически воспринимается лучше, чем стандартный спиннер. В одном из моих проектов это простое изменение увеличило удовлетворенность пользователей даже при одинаковом времени загрузки!
Комбинируя все эти подходы, вы сможете создать действительно отзывчивый, доступный и приятный в использовании интерфейс автозаполнения, который будет работать одинаково хорошо на всех устройствах.
Тестирование и мониторинг ИИ-функциональности
Тестирование компонентов с ИИ-функциональностью — это отдельная головная боль, с которой я столкнулся практически сразу после внедрения автозаполнения в проект. Представьте ситуацию: вы пишете тест, который проверяет, что при вводе "Привет, как" компонент выдает подсказку. Запускаете тест — всё работает. Запускаете через час — тест падает, потому что ИИ выдал другой вариант. Знакомо, да?
Юнит-тесты и особенности тестирования ИИ-компонентов
Первое, что нужно понять — традиционные подходы к тестированию здесь не работают. Вы не можете ожидать детерминированных результатов от недетерминированной системы. Но это не значит, что тестировать нельзя.
Вот мой подход к юнит-тестированию компонентов с ИИ-автозаполнением:
| JavaScript | 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
| // AutocompleteInput.test.js
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import AutocompleteInput from './AutocompleteInput';
import * as api from '../api/autocomplete';
// Мокаем функцию API
jest.mock('../api/autocomplete');
describe('AutocompleteInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('должен отображаться без подсказок при коротком вводе', () => {
render(<AutocompleteInput />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'пр' } });
expect(screen.queryByTestId('suggestion')).not.toBeInTheDocument();
expect(api.fetchSuggestions).not.toHaveBeenCalled();
});
test('должен запрашивать подсказки при достаточном вводе', async () => {
// Подготавливаем мок ответа
api.fetchSuggestions.mockResolvedValue(' мир!');
render(<AutocompleteInput />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'Привет' } });
// Проверяем, что запрос был отправлен с правильными параметрами
await waitFor(() => {
expect(api.fetchSuggestions).toHaveBeenCalledWith('Привет');
});
// Проверяем, что подсказка отображается
const suggestion = await screen.findByTestId('suggestion');
expect(suggestion).toHaveTextContent(' мир!');
});
test('должен обрабатывать ошибки API корректно', async () => {
// Симулируем ошибку API
api.fetchSuggestions.mockRejectedValue(new Error('API error'));
render(<AutocompleteInput />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'Привет' } });
// Проверяем, что сообщение об ошибке отображается
const errorMsg = await screen.findByTestId('error-message');
expect(errorMsg).toBeInTheDocument();
});
}); |
|
Заметьте, что я не проверяю конкретный контент подсказки, а только факт её получения и отображения. Это ключевой момент — тестируйте поведение, а не конкретные ответы ИИ.
Моккинг ИИ-ответов для стабильного тестирования
В CI/CD пайплайнах стабильность тестов критична. Я разработал несколько стратегий моккинга ИИ-ответов:
1. Предопределенные ответы для конкретных запросов
| JavaScript | 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
| // mocks/autocompleteResponses.js
export const mockResponses = {
'Привет': ' мир!',
'Как дела': '? У меня всё отлично',
'React это': ' JavaScript-библиотека для создания интерфейсов',
// другие пары запрос-ответ
};
// autocomplete.js
export const fetchSuggestions = async (input) => {
// В тестовом режиме используем моки
if (process.env.NODE_ENV === 'test') {
return mockResponses[input] || '';
}
// Реальный запрос к API
const response = await fetch('/api/autocomplete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: input }),
});
const data = await response.json();
return data.completion;
}; |
|
2. Генерация детерминированных ответов на основе хеша запроса
Этот подход более гибкий и не требует предопределения всех возможных запросов:
| JavaScript | 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
| // deterministicMock.js
import { createHash } from 'crypto';
export function generateDeterministicResponse(input, options = {}) {
const {
minLength = 3,
maxLength = 20,
seed = 'fixed-seed-for-tests'
} = options;
// Создаем детерминированный хеш на основе входных данных
const hash = createHash('md5').update(input + seed).digest('hex');
// Определяем длину ответа на основе хеша
const responseLength = minLength + (parseInt(hash.substring(0, 4), 16) % (maxLength - minLength));
// Генерируем псевдослучайный ответ фиксированной длины
let result = '';
const characters = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя ,.!?';
for (let i = 0; i < responseLength; i++) {
const charIndex = parseInt(hash.substring(i * 2, i * 2 + 2), 16) % characters.length;
result += characters.charAt(charIndex);
}
return result;
} |
|
Этот код генерирует "случайные", но детерминированные ответы для одинаковых входных данных, что идеально для тестирования.
Интеграционное тестирование
Интеграционные тесты проверяют взаимодействие компонентов и API. Для них я использую библиотеку MSW (Mock Service Worker), которая позволяет перехватывать сетевые запросы на уровне браузера:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // setupTests.js
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { mockResponses } from './mocks/autocompleteResponses';
const server = setupServer(
rest.post('/api/autocomplete', (req, res, ctx) => {
const { prompt } = req.body;
return res(
ctx.json({
completion: mockResponses[prompt] || 'Стандартный ответ для тестов'
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close()); |
|
Этот подход позволяет тестировать компоненты так, будто они действительно общаются с сервером, но с предсказуемыми ответами.
Метрики производительности и их мониторинг
В одном проекте я столкнулся с неожиданой проблемой — автозаполнение работало отлично на моей машине, но тормозило у пользователей. Оказалось, что дело в разнице задержек запросов. С тех пор я всегда мониторю следующие метрики:
1. Время ответа API — сколько времени занимает получение ответа от OpenAI
2. Время рендеринга — как быстро React отрисовывает полученные подсказки
3. Общая задержка ввода — время между нажатием клавиши и появлением подсказки
Вот простой хук для измерения этих метрик:
| JavaScript | 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
| function usePerformanceMetrics() {
const [metrics, setMetrics] = useState({
apiResponseTime: 0,
renderTime: 0,
totalLatency: 0
});
const measureApiCall = async (apiCallFn, ...args) => {
const startTime = performance.now();
const result = await apiCallFn(...args);
const endTime = performance.now();
setMetrics(prev => ({
...prev,
apiResponseTime: endTime - startTime
}));
return result;
};
const measureRender = (callback) => {
const startTime = performance.now();
requestAnimationFrame(() => {
const endTime = performance.now();
setMetrics(prev => ({
...prev,
renderTime: endTime - startTime
}));
});
callback();
};
const startLatencyMeasurement = () => {
return performance.now();
};
const endLatencyMeasurement = (startTime) => {
const endTime = performance.now();
setMetrics(prev => ({
...prev,
totalLatency: endTime - startTime
}));
};
return { metrics, measureApiCall, measureRender, startLatencyMeasurement, endLatencyMeasurement };
} |
|
Собраные метрики я отправляю в систему мониторинга (например, Sentry или самописное решение) и анализирую тренды. Это позволяет заметить деградацию производительности до того, как пользователи начнут жаловаться.
Анализ качества ИИ-ответов
Самая сложная часть — оценка качества самих предложений. Я разработал систему, которая собирает статистику по трем ключевым метрикам:
1. Точность — как часто пользователи принимают предложения без изменений.
2. Релевантность — насколько предложения соответствуют контексту (оценивается косвенно).
3. Пользовательское принятие — общая статистика использования функции.
| JavaScript | 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
| class SuggestionQualityAnalyzer {
constructor() {
this.stats = {
totalSuggestions: 0,
acceptedSuggestions: 0,
partiallyAcceptedSuggestions: 0,
rejectedSuggestions: 0,
contextRelevance: [] // Оценки от 0 до 1
};
}
recordSuggestion(suggestion, userInput, finalText) {
this.stats.totalSuggestions++;
// Проверяем, принял ли пользователь предложение
if (finalText === userInput + suggestion) {
this.stats.acceptedSuggestions++;
}
// Проверяем частичное принятие
else if (finalText.includes(userInput + suggestion.substring(0, suggestion.length / 2))) {
this.stats.partiallyAcceptedSuggestions++;
}
// Иначе считаем отвергнутым
else {
this.stats.rejectedSuggestions++;
}
// Отправляем данные на сервер для анализа
this.sendStatsToServer();
}
// Другие методы анализа и отчетности
} |
|
Но заметьте, что автоматический анализ релевантности — это нетривиальная задача. В одном проекте мы внедрили механизм, где другая модель ИИ оценивала релевантность предложений первой модели — своеобразный "ИИ для проверки ИИ". Хотя это и увеличило расходы на API, но значительно повысило качество предложений.
Такжен, важной частью мониторинга стал периодический ручной анализ логов — я просматриваю случаи, когда пользователи отвергают предложения, и пытаюсь найти паттерны для улучшения промптов или логики подсказок.
A/B тестирование разных подходов к автозаполнению
Один из самых эффективных способов улучшить качество автозаполнения — это A/B тестирование разных параметров и промптов. Я реализовал простую систему для этого:
| JavaScript | 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
| function useABTesting(testName, variants, userIdOrSeed) {
// Детерминированный выбор варианта для конкретного пользователя
const variantIndex = Math.abs(
hashCode(testName + userIdOrSeed) % variants.length
);
const selectedVariant = variants[variantIndex];
useEffect(() => {
// Записываем, какой вариант был показан пользователю
logExposure(testName, selectedVariant.id, userIdOrSeed);
}, [testName, selectedVariant.id, userIdOrSeed]);
const logResult = (result) => {
// Записываем результат взаимодействия
logConversion(testName, selectedVariant.id, userIdOrSeed, result);
};
return { variant: selectedVariant, logResult };
}
// Пример использования
function AutocompleteWithABTest({ userId }) {
const { variant, logResult } = useABTesting('autocomplete_temperature', [
{ id: 'low_temp', temperature: 0.3, maxTokens: 15 },
{ id: 'medium_temp', temperature: 0.7, maxTokens: 20 },
{ id: 'high_temp', temperature: 1.0, maxTokens: 25 }
], userId);
// Используем параметры из выбранного варианта
const fetchSuggestions = async (input) => {
// ... используем variant.temperature и variant.maxTokens
};
// Логируем результаты взаимодействия
const handleAcceptSuggestion = () => {
logResult('accepted');
// ...
};
const handleRejectSuggestion = () => {
logResult('rejected');
// ...
};
// ...остальной код компонента
} |
|
Такой подход позволяет мне систематически улучшать качество предложений на основе реальных данных, а не интуиции.
Я убедился, что тестирование и мониторинг ИИ-функциональности — это не одноразовая задача, а непрерывный процесс. Чем больше данных вы собираете о реальном использовании, тем лучше становится ваше автозаполнение.
Безопасность и ограничения использования
Говоря откровенно, каждый раз, когда я интегрирую OpenAI API в проект, меня бросает в холодный пот от мысли: "А что, если API-ключ утечет?" Это не паранойя — я видел проекты, где утечка ключа приводила к счетам на тысячи долларов за считанные часы. Давайте поговорим о том, как этого избежать и о других важных аспектах безопасности и ограничениях при работе с ИИ-автозаполнением.
Защита API-ключей и стратегии безопасности
Первое правило работы с OpenAI API: никогда не храните ключи на клиенте. Звучит очевидно, но я встречал этот антипаттерн даже в проектах опытных разработчиков. Вот как выглядит неправильный подход:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // НЕ ДЕЛАЙТЕ ТАК!
const openai = new OpenAI({
apiKey: 'sk-abcdefg123456789' // Это прямой путь к катастрофе
});
// Запрос прямо из браузера
const fetchSuggestions = async (input) => {
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: input }]
});
return response.choices[0].message.content;
}; |
|
Даже если вы используете переменные окружения при сборке (.env файлы в React), ключ все равно окажется в финальном бандле JavaScript, доступном любому, кто умеет открывать инструменты разработчика в браузере.
Единственное правильное решение — использование прокси-сервера:
| JavaScript | 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
| // server.js - Безопасный бэкенд-прокси
require('dotenv').config();
const express = require('express');
const app = express();
const { OpenAI } = require('openai');
// Ключ доступен только серверу
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
// API-эндпоинт с дополнительной защитой
app.post('/api/autocomplete', async (req, res) => {
// Проверка авторизации
if (!isAuthorized(req)) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Валидация входных данных
const { prompt } = req.body;
if (!prompt || typeof prompt !== 'string') {
return res.status(400).json({ error: 'Invalid prompt' });
}
try {
// Обращение к OpenAI
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }]
});
res.json({ completion: response.choices[0].message.content });
} catch (error) {
console.error('OpenAI Error:', error);
res.status(500).json({ error: 'Service unavailable' });
}
}); |
|
Для дополнительной безопасности я рекомендую:
1. Настроить ограничения по IP — разрешить запросы только с определенных адресов,
2. Внедрить токены доступа с коротким сроком жизни — даже если токен перехватят, он быстро станет бесполезным,
3. Настроить мониторинг расходов — чтобы быстро среагировать на подозрительную активность.
Контроль расходов на запросы
Ох, этот момент, когда я открыл первый счет за использование OpenAI API в продакшене! С тех пор я разработал несколько стратегий контроля расходов:
1. Установка жестких лимитов
OpenAI позволяет установить месячные лимиты расходов. Обязательно настройте их в панели управления, это ваша страховка от неприятных сюрпризов.
2. Кэширование запросов
Кэширование может значительно снизить количество запросов:
| JavaScript | 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
| // Простая реализация кэша запросов
const suggestionCache = new Map();
app.post('/api/autocomplete', async (req, res) => {
const { prompt } = req.body;
// Проверяем кэш перед запросом к API
const cacheKey = prompt.trim().toLowerCase();
if (suggestionCache.has(cacheKey)) {
return res.json({ completion: suggestionCache.get(cacheKey) });
}
try {
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }]
});
const completion = response.choices[0].message.content;
// Сохраняем в кэше
suggestionCache.set(cacheKey, completion);
// Ограничиваем размер кэша
if (suggestionCache.size > 10000) {
const oldestKey = suggestionCache.keys().next().value;
suggestionCache.delete(oldestKey);
}
res.json({ completion });
} catch (error) {
// Обработка ошибок
}
}); |
|
3. Квотирование по пользователям
Для многопользовательских приложений я всегда реализую систему квот:
| JavaScript | 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
| // Упрощенная система квотирования
const userQuotas = new Map();
function checkAndUpdateQuota(userId) {
// Получаем или инициализируем квоту пользователя
if (!userQuotas.has(userId)) {
userQuotas.set(userId, {
dailyRequests: 0,
lastReset: Date.now()
});
}
const quota = userQuotas.get(userId);
// Сбрасываем счетчик, если прошел день
const oneDayMs = 24 * 60 * 60 * 1000;
if (Date.now() - quota.lastReset > oneDayMs) {
quota.dailyRequests = 0;
quota.lastReset = Date.now();
}
// Проверяем квоту
const dailyLimit = getUserDailyLimit(userId); // зависит от типа подписки и т.д.
if (quota.dailyRequests >= dailyLimit) {
return false; // Квота исчерпана
}
// Увеличиваем счетчик запросов
quota.dailyRequests++;
return true;
} |
|
4. Умное управление токенами
Стоимость запросов к OpenAI напрямую зависит от количества токенов. Я оптимизирую промпты и ограничиваю максимальное количество токенов в ответе:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Определяем примерное количество токенов в промпте
function estimateTokenCount(text) {
// Грубая оценка: 1 токен ~ 4 символа для латиницы, ~1.5 символа для кириллицы
const latinChars = text.replace(/[^\x00-\x7F]/g, '').length;
const nonLatinChars = text.length - latinChars;
return Math.ceil(latinChars / 4 + nonLatinChars / 1.5);
}
// Адаптивно регулируем max_tokens в зависимости от длины промпта
function adaptiveTokenLimit(prompt) {
const estimatedPromptTokens = estimateTokenCount(prompt);
// Ограничиваем общее количество токенов в запросе
const maxTotalTokens = 1000; // зависит от модели и бюджета
const maxTokensForResponse = Math.max(20, maxTotalTokens - estimatedPromptTokens);
return Math.min(maxTokensForResponse, 100); // не более 100 токенов в ответе
} |
|
Приватность данных и этические аспекты
Вопрос приватности при работе с ИИ-автозаполнением стоит особенно остро. Вы отправляете текст пользователя в OpenAI, и нужно понимать, что эти данные могут использоваться для обучения моделей (если вы не используете специальные API с гарантиями сохранности данных).
Я всегда следую нескольким принципам:
1. Информированное согласие — явно сообщаю пользователям, что их ввод отправляется в OpenAI.
2. Минимизация данных — отправляю только необходимый минимум информации, без персональных данных.
3. Возможность отключения — даю пользователям возможность отказаться от ИИ-автозаполнения.
| JavaScript | 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
| function PrivacyAwareAutocomplete() {
const [useAI, setUseAI] = useState(
localStorage.getItem('useAIAutocomplete') !== 'false'
);
const toggleAI = () => {
const newValue = !useAI;
setUseAI(newValue);
localStorage.setItem('useAIAutocomplete', newValue.toString());
};
return (
<div>
<div className="privacy-toggle">
<label>
<input
type="checkbox"
checked={useAI}
onChange={toggleAI}
/>
Использовать ИИ-автозаполнение (данные отправляются в OpenAI)
</label>
</div>
{useAI ? (
<AIEnabledInput />
) : (
<StandardInput />
)}
</div>
);
} |
|
Технические ограничения моделей
После нескольких лет работы с API OpenAI я хорошо знаю его ограничения:
1. Задержка ответа — даже самые быстрые запросы к GPT занимают 200-500 мс, что заметно при интерактивном автозаполнении
2. Непоследовательность — модель может давать разные ответы на одинаковые запросы, что затрудняет создание стабильного UX
3. Ограничения контекста — модель не хранит состояние между запросами, если вы сами его не поддерживаете
4. Rate limiting — OpenAI ограничивает количество запросов в единицу времени
Я разработал несколько стратегий для смягчения этих ограничений:
Обработка Rate Limiting
| JavaScript | 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
| // Адаптивная стратегия для обхода rate limiting
class AdaptiveRateLimiter {
constructor() {
this.requestLog = [];
this.currentLimit = 60; // начальное значение: 60 запросов в минуту
this.waitTime = 0;
}
async processRequest(requestFn) {
// Очищаем историю старых запросов
const now = Date.now();
this.requestLog = this.requestLog.filter(time => now - time < 60000);
// Если превысили текущий лимит, ждем
if (this.requestLog.length >= this.currentLimit) {
const oldestRequest = this.requestLog[0];
const timeToWait = 60000 - (now - oldestRequest) + 100; // +100ms для надежности
this.waitTime = Math.max(this.waitTime, timeToWait);
await new Promise(resolve => setTimeout(resolve, this.waitTime));
}
try {
// Выполняем запрос
const result = await requestFn();
// Успешный запрос - возможно, можно увеличить лимит
this.waitTime = Math.max(0, this.waitTime - 50); // постепенно уменьшаем время ожидания
this.requestLog.push(Date.now());
return result;
} catch (error) {
// Если получили ошибку rate limit, адаптируем наши параметры
if (error.message.includes('rate_limit')) {
this.currentLimit = Math.floor(this.currentLimit * 0.8); // снижаем лимит на 20%
this.waitTime += 500; // увеличиваем время ожидания
}
throw error;
}
}
} |
|
Локальные альтернативы для основных сценариев
Для критически важных функций я всегда имею запасной локальный вариант автозаполнения, который работает без внешних API:
| JavaScript | 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
| // Простая n-граммная модель для предсказания следующего слова
class NGramPredictor {
constructor(trainingData, n = 3) {
this.n = n;
this.ngramCounts = new Map();
this.train(trainingData);
}
train(text) {
const words = text.split(/\s+/);
for (let i = 0; i <= words.length - this.n; i++) {
const ngram = words.slice(i, i + this.n - 1).join(' ');
const nextWord = words[i + this.n - 1];
if (!this.ngramCounts.has(ngram)) {
this.ngramCounts.set(ngram, new Map());
}
const countMap = this.ngramCounts.get(ngram);
countMap.set(nextWord, (countMap.get(nextWord) || 0) + 1);
}
}
predict(text) {
const words = text.split(/\s+/);
const prefix = words.slice(-this.n + 1).join(' ');
if (!this.ngramCounts.has(prefix)) {
return ''; // Нет предсказания
}
const countMap = this.ngramCounts.get(prefix);
let bestWord = '';
let bestCount = 0;
for (const [word, count] of countMap.entries()) {
if (count > bestCount) {
bestCount = count;
bestWord = word;
}
}
return bestWord;
}
} |
|
Эта примитивная модель может служить запасным вариантом, когда API OpenAI недоступен или исчерпаны квоты.
Несовместимость React-Router и React-Bootstrap Добрый день,
Пишу маленький проект и в качестве дизайна решил использовать React-Bootstrap.
При... Objects are not valid as a React child (found: TypeError: response[0].includes is not a function). REACT Всем привет. Создаю страничку на React. Смысл работы примерно таков : пользователь заходит,... Посоветуйте практический курс на React redux/ react Всем привет. Столкнулся с тем, что мне не хватает практики. Подскажите какой практический курс по... Разница между React и React native Я хочу начать освоение React для фрондента, но при этом хотел бы иметь возможность писать мобильные... react/ react hook с Rxjs Здравствуйте.
Столкнулся с проблемой изучения библиотеки RxJs.
У меня есть ТЗ, создать... React.createContext или import { createContext } from "react" в чём разница? import React from 'react';
const AuthContext = React.createContext();
or
import {... Не переходит по страницам TS React react-router Здравствуйте, не могу понять в чём моя проблема почему у меня не переходит со страницы Главная на... Ошибка при создании проекта React с помощью пакета create-react-app Привет. Пытаюсь изучать JavaScript. Дошёл до библиотеки React. Пытаюсь создать первое приложение.... Автозаполнение выпадающего списка! Хочу заполнить выпадающий список числами от 1-31 с помощью цикла,вот попробовал решить проблему но... Не работает автозаполнение полей JQuery Здравствуйте не работает скрипт автозаполнение полей, подскажите пожалуйста в чем дело
<script... IFRAME и автозаполнение в формах... У меня есть форма, расположенная в IFRAME, который расположен внизу страницы.
Выпадающий список... Автозаполнение поля второго поля данными из первого Есть два поля Input1 и Input2
Нужно чтобы весь текст печатаемый в 1-м печатался во втором...
|