Перейти к основному содержимому

Course Creation

AI-помощник для создания курсов с SSE streaming.

Структура

src/features/course-creation/
├── api/
│ ├── queries.ts # checkCanCreateCourse()
│ └── mutations.ts # createCourse()
├── hooks/
│ └── use-progress-subscription.ts # SSE для прогресса
├── components/
│ ├── creation-fab.tsx # Плавающая кнопка
│ ├── creation-panel.tsx # UI wizard
│ ├── wizard-message.tsx # Сообщение помощника
│ ├── quick-replies.tsx # Быстрые ответы
│ ├── progress-indicator.tsx # Индикатор создания
│ └── limit-modal.tsx # Модалка лимита
├── constants.ts
├── types.ts
└── index.ts

Wizard Flow

┌─────────────────────────────────────────────────────┐
│ 1. Тема курса │
│ "О чём будет курс? Например: Python для новичков" │
└─────────────────────┬───────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 2. Язык курса │
│ [Русский] [English] [Українська] │
└─────────────────────┬───────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 3. Сложность │
│ [Начинающий] [Средний] [Продвинутый] │
└─────────────────────┬───────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 4. AI генерирует курс │
│ [Создаю структуру...] [Генерирую модули...] │
└─────────────────────┬───────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 5. Курс готов! │
│ [Открыть курс] [Закрыть] │
└─────────────────────────────────────────────────────┘

Zustand Store

// src/stores/course-creation-store.ts
interface CourseCreationStore {
step: 'topic' | 'language' | 'difficulty' | 'generating' | 'complete';
topic: string;
language: 'en' | 'ru' | 'uk';
difficulty: 'beginner' | 'intermediate' | 'advanced';
progress: number;
progressMessage: string;
createdCourseId: string | null;
setTopic: (topic: string) => void;
setLanguage: (language: string) => void;
setDifficulty: (difficulty: string) => void;
setProgress: (progress: number, message: string) => void;
setComplete: (courseId: string) => void;
reset: () => void;
}

SSE Progress

// hooks/use-progress-subscription.ts
import { fetch as expoFetch } from 'expo/fetch';

export function useProgressSubscription(onProgress: (p: Progress) => void) {
const subscribe = async (courseId: string) => {
const response = await expoFetch(
`${API_URL}/courses/${courseId}/generation-progress`,
{
headers: { Authorization: `Bearer ${token}` },
}
);

const reader = response.body?.getReader();
const decoder = new TextDecoder();

while (true) {
const { done, value } = await reader!.read();
if (done) break;

const chunk = decoder.decode(value);
const lines = chunk.split('\n');

for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
onProgress(data);
}
}
}
};

return { subscribe };
}

API

checkCanCreateCourse

Проверка лимита перед созданием:

export async function checkCanCreateCourse(userId: string): Promise<boolean> {
const { data: user } = await supabase
.from('users')
.select('subscription_tier, courses(count)')
.eq('id', userId)
.single();

const limit = COURSE_LIMITS[user.subscription_tier];
return user.courses[0].count < limit;
}

createCourse

Запуск генерации:

export async function createCourse(params: CreateCourseParams) {
const response = await fetch(`${API_URL}/courses/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
topic: params.topic,
language: params.language,
difficulty: params.difficulty,
}),
});

return response.json(); // { courseId: string }
}

Компоненты

CreationFAB

function CreationFAB() {
const [visible, setVisible] = useState(false);
const canCreate = useCanCreateCourse();

const handlePress = () => {
if (!canCreate) {
showLimitModal();
} else {
setVisible(true);
}
};

return (
<>
<Pressable
className="absolute bottom-6 right-6 w-14 h-14 bg-primary rounded-full items-center justify-center shadow-lg"
onPress={handlePress}
>
<Ionicons name="add" size={28} color="white" />
</Pressable>

<Modal visible={visible}>
<CreationPanel onClose={() => setVisible(false)} />
</Modal>
</>
);
}

ProgressIndicator

function ProgressIndicator() {
const { progress, progressMessage } = useCourseCreationStore();

return (
<View className="items-center p-6">
<ActivityIndicator size="large" color={colors.primary} />

<ProgressBar value={progress} className="w-full mt-4" />

<Text className="text-gray-600 mt-2">{progressMessage}</Text>
</View>
);
}

QuickReplies

function QuickReplies({ step, onSelect }) {
const options = {
language: [
{ label: 'Русский', value: 'ru' },
{ label: 'English', value: 'en' },
{ label: 'Українська', value: 'uk' },
],
difficulty: [
{ label: 'Начинающий', value: 'beginner' },
{ label: 'Средний', value: 'intermediate' },
{ label: 'Продвинутый', value: 'advanced' },
],
};

return (
<View className="flex-row flex-wrap gap-2">
{options[step]?.map(opt => (
<Button key={opt.value} variant="outline" onPress={() => onSelect(opt.value)}>
<Text>{opt.label}</Text>
</Button>
))}
</View>
);
}

Лимиты

// Проверка лимита курсов
function useCanCreateCourse() {
const { tier } = useSubscription();
const { data: courses } = useCourses();

const limit = COURSE_LIMITS[tier];
return (courses?.length || 0) < limit;
}

// При достижении лимита
function LimitModal({ onUpgrade }) {
return (
<View>
<Text>Вы достигли лимита курсов</Text>
<Text>Обновите подписку чтобы создавать больше</Text>
<Button onPress={onUpgrade}>Обновить план</Button>
</View>
);
}