diff --git a/.env.example b/.env.example index c8b9f6790..b008cc263 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,20 @@ -# REQUIRED - Sandboxes for code execution -# Get yours at https://e2b.dev -E2B_API_KEY=your_e2b_api_key_here +# Переменные окружения для Railway -# REQUIRED - Web scraping for cloning websites -# Get yours at https://firecrawl.dev +# Обязательные переменные: +E2B_API_KEY=your_e2b_api_key_here FIRECRAWL_API_KEY=your_firecrawl_api_key_here -# OPTIONAL - AI Providers (need at least one) -# Get yours at https://console.anthropic.com +# Дополнительные ИИ провайдеры (нужен минимум один): ANTHROPIC_API_KEY=your_anthropic_api_key_here - -# Get yours at https://platform.openai.com OPENAI_API_KEY=your_openai_api_key_here - -# Get yours at https://aistudio.google.com/app/apikey GEMINI_API_KEY=your_gemini_api_key_here +GROQ_API_KEY=your_groq_api_key_here + +# Supabase Configuration +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here -# Get yours at https://console.groq.com -GROQ_API_KEY=your_groq_api_key_here \ No newline at end of file +# Railway переменные +NODE_ENV=production +PORT=3000 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 000000000..040cbe85c --- /dev/null +++ b/.env.production @@ -0,0 +1,5 @@ +# This file is used by Railway for production builds +# Railway will inject these from environment variables +NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} +NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} +SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..5d91b96e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.{tsx,ts,js,jsx,json,css,md} text eol=lf encoding=utf-8 diff --git a/.gitignore b/.gitignore index ac59fa8fa..d5170b1f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,58 +1,98 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -**/node_modules/ -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Production +.next/ +out/ +build/ +dist/ -# debug +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* -.env.local -!.env.example +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm -# vercel -.vercel +# Optional eslint cache +.eslintcache -# typescript -*.tsbuildinfo -next-env.d.ts +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ -# E2B template builds -*.tar.gz -e2b-template-* +# Optional REPL history +.node_repl_history -# IDE +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files .vscode/ .idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db -# Temporary files -*.tmp -*.temp -repomix-output.txt -bun.lockb +# TypeScript +*.tsbuildinfo \ No newline at end of file diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md new file mode 100644 index 000000000..864c1f475 --- /dev/null +++ b/AUTH_SETUP.md @@ -0,0 +1,190 @@ +# рџ”ђ Система аутентификации + +## вњ… Что было создано + +### 1. Компоненты аутентификации +- **`components/auth/AuthModal.tsx`** - модальное РѕРєРЅРѕ РІС…РѕРґР°/регистрации +- **`components/auth/UserButton.tsx`** - РєРЅРѕРїРєР° пользователя СЃ выпадающим меню +- **`components/PageHeader.tsx`** - хедер страницы СЃ РєРЅРѕРїРєРѕР№ пользователя + +### 2. Контекст пользователя +- **`contexts/AuthContext.tsx`** - управление состоянием аутентификации +- Автоматическое отслеживание сессии +- Глобальный доступ Рє данным пользователя + +### 3. Страница личного кабинета +- **`app/dashboard/page.tsx`** - личный кабинет пользователя +- РЎРїРёСЃРѕРє проектов +- Статистика +- Управление проектами + +### 4. Интеграция +- **`app/layout.tsx`** - обернут РІ `AuthProvider` +- Готово Рє использованию РІРѕ всем приложении + +## рџљЂ Как использовать + +### Вариант 1: Использовать готовый компонент PageHeader + +Р’ `app/page.tsx` замените существующий хедер РЅР°: + +```tsx +import PageHeader from '@/components/PageHeader'; + +// Р’ компоненте: + { + setAiModel(model); + // обновить URL параметры + }} + availableModels={appConfig.ai.availableModels} + modelDisplayNames={appConfig.ai.modelDisplayNames} + onCreateSandbox={createSandbox} + onReapply={reapplyLastGeneration} + onDownloadZip={downloadZip} + canReapply={!!conversationContext.lastGeneratedCode && !!sandboxData} + hasSandbox={!!sandboxData} +/> +``` + +### Вариант 2: Добавить только РєРЅРѕРїРєСѓ пользователя + +Просто добавьте РІ РЅСѓР¶РЅРѕРµ место: + +```tsx +import UserButton from '@/components/auth/UserButton'; + +// Р’ JSX: + +``` + +## рџ“± Функционал + +### Для неавторизованных пользователей: +- РљРЅРѕРїРєР° "Войти" +- Модальное РѕРєРЅРѕ СЃ формами РІС…РѕРґР°/регистрации +- Переключение между РІС…РѕРґРѕРј Рё регистрацией +- Валидация email Рё пароля + +### Для авторизованных пользователей: +- Аватар СЃ первой Р±СѓРєРІРѕР№ email +- Выпадающее меню СЃ: + - Email пользователя + - Ссылка РЅР° личный кабинет + - РљРЅРѕРїРєР° выхода + +## рџЏ  Личный кабинет + +Доступен РїРѕ адресу: `/dashboard` + +**Функции:** +- Просмотр всех проектов пользователя +- Статистика (всего, активных, РІ архиве) +- Ссылки РЅР° preview проектов +- Информация Рѕ sandbox'ах +- РљРЅРѕРїРєР° создания РЅРѕРІРѕРіРѕ проекта + +## рџ”’ Защита роутов + +Личный кабинет автоматически перенаправляет неавторизованных пользователей РЅР° главную страницу. + +## рџЋЁ Стилизация + +Р’СЃРµ компоненты используют: +- Tailwind CSS +- Dark mode support +- Адаптивный дизайн +- Анимации Рё transitions + +## рџ“ќ Использование РІ РєРѕРґРµ + +### Получить текущего пользователя: + +```tsx +import { useAuth } from '@/contexts/AuthContext'; + +function MyComponent() { + const { user, loading, signOut } = useAuth(); + + if (loading) return
Loading...
; + + if (!user) { + return
Пожалуйста, войдите
; + } + + return ( +
+

Привет, {user.email}!

+ +
+ ); +} +``` + +### Работа СЃ проектами: + +```tsx +import { getUserProjects, createProject } from '@/lib/supabase-projects'; + +// Получить проекты пользователя +const projects = await getUserProjects({ status: 'active' }); + +// Создать новый проект +const newProject = await createProject({ + name: 'My Project', + description: 'Project description', + sandbox_id: 'sandbox-123' +}); +``` + +## рџ”§ Настройка + +### Email подтверждение + +РџРѕ умолчанию Supabase требует подтверждение email. Чтобы отключить: + +1. Откройте [Supabase Dashboard](https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/auth/users) +2. Перейдите РІ **Authentication в†’ Settings** +3. Отключите **Enable email confirmations** + +### Кастомизация email шаблонов + +1. Откройте **Authentication в†’ Email Templates** +2. Настройте шаблоны для: + - Подтверждение регистрации + - РЎР±СЂРѕСЃ пароля + - Изменение email + +## рџЋЇ Следующие шаги + +1. **Замените хедер** РІ `app/page.tsx` РЅР° `PageHeader` компонент +2. **Примените миграцию** РёР· `supabase/migrations/001_initial_schema.sql` +3. **Настройте email** РІ Supabase Dashboard +4. **Протестируйте** регистрацию Рё РІС…РѕРґ + +## рџђ› Troubleshooting + +### Ошибка "User already registered" +- Пользователь СѓР¶Рµ существует, используйте РІС…РѕРґ вместо регистрации + +### РќРµ РїСЂРёС…РѕРґРёС‚ email подтверждения +- Проверьте настройки SMTP РІ Supabase +- Проверьте папку спам +- Отключите email подтверждение для тестирования + +### РќРµ работает РІС…РѕРґ +- Убедитесь, что переменные окружения Supabase установлены +- Проверьте консоль браузера РЅР° ошибки +- Убедитесь, что RLS политики настроены правильно + +## рџ“љ Дополнительные ресурсы + +- [Supabase Auth Docs](https://supabase.com/docs/guides/auth) +- [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security) +- [Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) + +--- + +**Готово!** Система аутентификации полностью настроена Рё готова Рє использованию. рџЋ‰ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..23e6d440d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Используем стандартный Node.js образ (не Alpine) для совместимости с lightningcss +FROM node:20-slim + +# Устанавливаем необходимые системные пакеты для нативных модулей +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем package.json и package-lock.json +COPY package*.json ./ + +# Устанавливаем все зависимости и пересобираем нативные модули +RUN npm ci && npm rebuild + +# Копируем исходный код +COPY . . + +# Собираем приложение +RUN npm run build + +# Удаляем devDependencies после сборки +RUN npm ci --omit=dev && npm cache clean --force + +# Указываем порт +EXPOSE 3000 + +# Запускаем приложение +CMD ["npm", "start"] diff --git a/Dockerfile.extended b/Dockerfile.extended new file mode 100644 index 000000000..d8182d1a3 --- /dev/null +++ b/Dockerfile.extended @@ -0,0 +1,33 @@ +# Альтернативный Dockerfile с поддержкой lightningcss +FROM node:20-slim + +# Устанавливаем необходимые системные пакеты для lightningcss +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем package.json и package-lock.json +COPY package*.json ./ + +# Устанавливаем все зависимости +RUN npm ci + +# Копируем исходный код +COPY . . + +# Собираем приложение +RUN npm run build + +# Удаляем devDependencies после сборки +RUN npm ci --omit=dev && npm cache clean --force + +# Указываем порт +EXPOSE 3000 + +# Запускаем приложение +CMD ["npm", "start"] diff --git a/Dockerfile.simple b/Dockerfile.simple new file mode 100644 index 000000000..1d853a9c1 --- /dev/null +++ b/Dockerfile.simple @@ -0,0 +1,29 @@ +# Простой Dockerfile без проблем с lightningcss +FROM node:20-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем package.json и package-lock.json +COPY package*.json ./ + +# Устанавливаем зависимости +RUN npm ci + +# Копируем исходный код +COPY . . + +# Устанавливаем переменную окружения для отключения telemetry +ENV NEXT_TELEMETRY_DISABLED=1 + +# Собираем приложение без оптимизаций CSS +RUN npm run build + +# Удаляем devDependencies после сборки +RUN npm ci --omit=dev && npm cache clean --force + +# Указываем порт +EXPOSE 3000 + +# Запускаем приложение +CMD ["npm", "start"] diff --git a/INTEGRATION_GUIDE.md b/INTEGRATION_GUIDE.md new file mode 100644 index 000000000..ce0f6c7c7 --- /dev/null +++ b/INTEGRATION_GUIDE.md @@ -0,0 +1,95 @@ +# рџ”§ Инструкция РїРѕ интеграции РєРЅРѕРїРєРё РІС…РѕРґР°/регистрации + +## вњ… Что СѓР¶Рµ готово: + +1. вњ… Создан компонент `HomeScreenHeader` СЃ РєРЅРѕРїРєРѕР№ РІС…РѕРґР° +2. вњ… Создан компонент `UserButton` СЃ функционалом аутентификации +3. вњ… Р’СЃРµ необходимые импорты добавлены + +## рџ“ќ Что РЅСѓР¶РЅРѕ сделать: + +### Шаг 1: Откройте файл `app/page.tsx` + +### Шаг 2: Найдите строку 27 Рё добавьте РёРјРїРѕСЂС‚: + +**Добавьте после строки 27:** +```tsx +import HomeScreenHeader from '@/components/HomeScreenHeader'; +``` + +Должно получиться: +```tsx +import { ThemeLogo } from '@/app/components/theme-logo'; +import UserButton from '@/components/auth/UserButton'; +import HomeScreenHeader from '@/components/HomeScreenHeader'; +``` + +### Шаг 3: Найдите строки 3032-3044 (хедер РЅР° home screen) + +**Найдите этот блок:** +```tsx +{/* Header */} +
+ + + + Использовать этот шаблон + +
+``` + +**Замените РЅР°:** +```tsx +{/* Header */} + +``` + +### Шаг 4: Сохраните файл + +### Шаг 5: Проверьте результат + +Теперь РІ правом верхнем углу главного экрана должна появиться РєРЅРѕРїРєР° "Войти" (или аватар пользователя, если РІС‹ СѓР¶Рµ авторизованы). + +## рџЋЇ Результат: + +- **Для неавторизованных пользователей**: РєРЅРѕРїРєР° "Войти" +- **Для авторизованных пользователей**: аватар СЃ выпадающим меню + +## рџ”Ќ Где найти нужные строки: + +### РЎРїРѕСЃРѕР± 1: РџРѕРёСЃРє РїРѕ тексту +Нажмите `Ctrl+F` Рё найдите текст: `Использовать этот шаблон` + +### РЎРїРѕСЃРѕР± 2: Переход Рє строке +Нажмите `Ctrl+G` Рё введите номер строки: `3032` + +### РЎРїРѕСЃРѕР± 3: РџРѕРёСЃРє РїРѕ комментарию +Найдите комментарий: `{/* Header */}` РЅР° главном экране (home screen) + +## вљ пёЏ Важно: + +Убедитесь, что РІС‹ редактируете правильный хедер - тот, который находится внутри блока `{showHomeScreen && (...)}` + +## 🆘 Если что-то пошло РЅРµ так: + +1. Убедитесь, что РёРјРїРѕСЂС‚ `HomeScreenHeader` добавлен РІ начале файла +2. Проверьте, что РІС‹ заменили правильный блок (РЅР° home screen, Р° РЅРµ РІ РѕСЃРЅРѕРІРЅРѕРј интерфейсе) +3. Сохраните файл Рё перезапустите dev сервер: `npm run dev` + +## вњ… После интеграции: + +Закоммитьте изменения: +```bash +git add . +git commit -m "feat: Replace GitHub button with auth button on home screen" +git push origin main +``` + +--- + +**Готово!** Теперь пользователи СЃРјРѕРіСѓС‚ войти или зарегистрироваться РїСЂСЏРјРѕ СЃ главного экрана! рџЋ‰ diff --git a/PAWIntegrationOverview.txt b/PAWIntegrationOverview.txt new file mode 100644 index 000000000..55a2e76eb --- /dev/null +++ b/PAWIntegrationOverview.txt @@ -0,0 +1,1354 @@ +Основные способы + интеграции с PayAnyWay + | Содержание | 2 + Содержание + Введение.....................................................................................................................................4 + Глава 1. PayAnyWay статическая кнопка....................................................5 + Формирование платежной кнопки через конструктор.............................................................................5 + Формирование платежной кнопки через MONETA.Assistant.................................................................5 + Добавление описания операции через MONETA.Assistant.....................................................................6 + Преобразование формы оплаты в ссылку.................................................................................................8 + Встраивание формы оплаты на страницу сайта.......................................................................................8 + Формирование платежной кнопки через SDK..........................................................................................8 + Платежная кнопка через SDK PHP................................................................................................8 + Платежная кнопка через SDK Python3..........................................................................................8 + Платежная кнопка через SDK Ruby...............................................................................................9 + Платежная кнопка через SDK Android..........................................................................................9 + Платежная кнопка через SDK PhoneGap.......................................................................................9 + Глава 2. PayAnyWay интеграция..................................................................10 + Интеграция PayAnyWay общие сведения...............................................................................................10 + Интеграция с CMS.....................................................................................................................................13 + Интеграция с PayAnyWay в мобильном приложении............................................................................13 + Реализация Pay URL..................................................................................................................................13 + Преобразование платежной формы в iframe...........................................................................................13 + Взаимодействие с виджетом оплаты, выводимым через iframe...........................................................14 + Особенности перехода на страницы Success, Fail URL при использовании виджета в + iframe..........................................................................................................................................14 + Динамические возможности Assistant.widget..............................................................................15 + Проверка наличия товара и его цены перед оплатой (Check URL)......................................................16 + Глава 3. PayAnyWay выписка счёта............................................................20 + Создание счёта через Merchant API.........................................................................................................20 + Пример кода на SDK php..............................................................................................................21 + Пример кода на SDK C#...............................................................................................................21 + Глава 4. PayAnyWay с холдированием........................................................22 + Пример кода на SDK php..........................................................................................................................23 + Глава 5. PayAnyWay регулярные платежи.................................................25 + Реализация регулярных платежей на SDK PHP.....................................................................................26 + Перед началом работы...................................................................................................................26 + Генерация формы рекурентного платежа....................................................................................26 + Генерация формы рекурентного платежа....................................................................................27 + Рассылка уведомлений о предстоящем платеже........................................................................27 + Осуществление регулярных платежей.........................................................................................27 + Обработчик отмены регулярного платежа..................................................................................27 + Ручное проведение регулярного платежа....................................................................................27 + Глава 6. PayAnyWay выплаты и списания................................................28 + Выплаты через Merchant API....................................................................................................................28 + Выплаты на банковскую карту.................................................................................................................29 + Списание с карты и выплаты для проектов с PCI DSS.........................................................................31 + | Содержание | 3 + Временное сохранение данных карты (в мобильном приложении).........................................31 + Списание с банковской карты через Merchant API....................................................................32 + Глава 7. PayAnyWay площадкам..................................................................33 + Глава 8. Полезные ссылки.............................................................................34 + SDK на различных языках........................................................................................................................34 + Документация по MONETA.Assistant......................................................................................................34 + Описание методов Merchant API..............................................................................................................34 + | +Введение | 4 + Введение + Документ дает информацию об основных способах интеграции с PayAnyWay. + Документ адресован разработчикам, имеющим базовые знания в области программирования. +Chapter + 1 + PayAnyWay статическая кнопка + Topics: + • Формирование платежной + кнопки через конструктор + • Формирование + платежной кнопки через + MONETA.Assistant + • Добавление описания + операции через + MONETA.Assistant + • Преобразование формы + оплаты в ссылку + • Встраивание формы + оплаты на страницу сайта + • Формирование платежной + кнопки через SDK + Формирование платежной кнопки через конструктор + Если Ваш проект предлагает небольшое количество товаров или услуг, то Вам может подойти простая + кнопка оплаты. + Html-код кнопки можно вставить на любой сайт, любую страницу любой сложности. Чтобы получить код + кнопки, нужно войти в личный кабинет системы Монета.ру, затем перейти в раздел “Рабочий кабинет”, + а затем на страницу “Список товаров и услуг”. + На этой странице добавьте Ваш товар, укажите стоимость и нажмите “Сохранить”. Затем перейдите на + страницу товара, кликнув на его наименование - здесь Вы увидите html код кнопки. Скопируйте его и + вставьте на страницу Вашего сайта: +
+
+ + + + +
+
+ Формирование платежной кнопки через MONETA.Assistant + Кнопку оплаты можно построить самостоятельно, не используя конструктор. Для этого нужно создать + html-форму со следующими обязательными полями: + • MNT_ID - Идентификатор магазина в системе MONETA.RU. Соответствует номеру расширенного + счета магазина. + | +PayAnyWay статическая кнопка | 6 + • MNT_AMOUNT - Сумма оплаты. Десятичные символы отделяются точкой. Количество знаков после + запятой - максимум два символа (если параметр не задан, то сумма будет запрошена в учетной системе + магазина соответствующим проверочным запросом на Check URL, который указывается в настройках + счёта в личном кабинете в системе Moneta.ru). + и необязательными: + • MNT_TRANSACTION_ID - Внутренний идентификатор заказа, однозначно определяющий заказ в + магазине. Ограничение на размер – 255 символов. + • MNT_CURRENCY_CODE - ISO код валюты, в которой производится оплата заказа в магазине. + Значение должно соответствовать коду валюты счета получателя (MNT_ID). Например, RUB, USD, + EUR. + • MNT_TEST_MODE - Указание, что запрос происходит в тестовом режиме. Возможные значения + 0 или 1. Если включено (значение 1), то реального списания и зачисления средств не произойдет. В + тестовом режиме не происходит передача управления от iframe с виджетом оплаты родительскому + окну. + • MNT_DESCRIPTION - Описание оплаты. Максимальная длина 500 символов. + Полный список полей и их описание приведено в документации MONETA.Assistant. + Ниже приводится пример формы, реализующей платежную кнопку, построенную на базе + Moneta.Assistant: +
+
+ + +

Сумма:

+ +
+
+ Обратите внимание на то, что форма отправляется на URL: https://moneta.ru/assistant.htm + Добавление описания операции через MONETA.Assistant + Если Вы хотите добавить поле для пользователя, в которое он будет вводить, его контактный телефон или + другую информацию, то добавьте следующую строчку в html-код Вашей формы: +

Номер телефона:

+ Перед строкой: + + Переданная пользователем информация будет записана в систему Монета.ру. Её можно будет + просматривать в истории операций по Вашему расширенному счёту. + Если на Вашем сайте допустима вставка JavaScript-кода, то Вы можете добавить не одно поле, а + несколько. Ниже приведён JavaScript-код, который перед нажатием на кнопку “Купить”, соберет все + предоставленные данные в параметр MNT_DESCRIPTION: + +

Номер телефона:

+

ФИО:

+

Комментарий:

+ + Данный код нужно вставить в html-код Вашей платежной формы перед строкой: + + Для проектов, которым достаточно для работы данного решения, нужно соответствующим образом + настроить расширенный счёт магазина в системе Moneta.ru. + Нужно установить “Тип интерфейса” в значение “MONETA.Assistant”, поле “Код проверки целостности + данных” нужно оставить пустым, а опцию “Подпись формы оплаты обязательна” установите в значение + “Нет”. + Чтобы защитить Вашу форму от изменения суммы покупателем, надо передать параметр MNT_AMOUNT + как скрытый: + + А также следует добавить в форму сигнатуру (подпись). + В этом случае настройки счета должны быть изменены. В поле “Код проверки целостности данных” + нужно ввести произвольное значение, а опцию “Подпись формы оплаты обязательна” установить в + значение “Да”. + Чтобы получить подпись платежной формы, надо выполнить следующий php-код: + + Сделать это (выполнить код) можно здесь: http://sandbox.onlinephpfunctions.com + Затем, полученный код, надо добавить в Вашу платежную форму: + + | +PayAnyWay статическая кнопка | 8 + Преобразование формы оплаты в ссылку + Если стоит задача сформировать не форму, а ссылку оплаты, то следует иметь ввиду, что + параметры платежной формы можно передавать через GET запрос. Например так: https://moneta.ru/ + shopping-11493408.htm?MNT_ITEM_ID=1182&MNT_QUANTITY=1 + Ссылка на MONETA.Assistant может быть такой: https://moneta.ru/assistant.htm? + MNT_ID=НОМЕР_РАСШИРЕННОГО_СЧЁТА&MNT_TEST_MODE=0&MNT_AMOUNT=12 + Можно добавить и описание операции по аналогии с формой оплаты: https://moneta.ru/assistant.htm? + MNT_ID=НОМЕР_РАСШИРЕННОГО_СЧЁТА&MNT_TEST_MODE=0&MNT_AMOUNT=12 + &MNT_DESCRIPTION=ОПИСАНИЕ_ОПЕРАЦИИ + Сформированную таким образом ссылку, можно отправить покупателю на e-mail, а также разместить в + социальной сети, например: Вконтакте, Instagram и т.п. + Встраивание формы оплаты на страницу сайта + Ссылку можно включить на свою html страницу через iframe с небольшим изменением, касающимся URL: + + В случае использования виджета через iframe, вместо кнопки “Купить” платежной формы, оплата будет + возможна сразу на Вашем сайте. + Содержимое iframe можно стилизовать индивидуально по Вашему усмотрению. Примеры страниц, + на которых выводится виджет MONETA.Assistant с измененным внешнем видом при помощи + дополнительного css-кода можно увидеть здесь: https://payanyway.ru/info/w/ru/public/w/partnership/ + developers/widget-css.html + Там же есть листинги css-кода по каждому из приведенных примеров. Для установки стилей на вашем + расширенном счете, необходимо прислать css-код для review в техническую поддержку. + Формирование платежной кнопки через SDK + Платежная кнопка через SDK PHP + Скачайте и настройте SDK PHP согласно инструкции. + Пример кода, формирующий кнопку “Оплатить”: + $result = $monetaSDK->showChoosePaymentSystemForm(); + echo $result->render; + Платежная кнопка через SDK Python3 + Скачайте и настройте SDK Python3 согласно инструкции. + Метод для отрисовки кнопки “Оплатить”: + result = templatePaymentFrom(orderId, amount, currency='RUB', description='', + paysys='') + Где + • orderId - номер операции, например 370429, + • amount - сумма операции, + • currency - валюта операции, например, RUB, + • description - описание операции, + | +PayAnyWay статическая кнопка | 9 + • paysys - заданный способ оплаты заказа, строка, например, 'plastic' для оплаты банковскими картами. + Платежная кнопка через SDK Ruby + Скачайте и настройте SDK Ruby согласно инструкции. + Чтобы получить ссылку на платежный шлюз для оплаты заказа пользователем, используйте метод: + Payanyway::Gateway.payment_url(params, use_signature = true) + Платежная кнопка через SDK Android + Скачайте и настройте SDK Android согласно инструкции. + Чтобы пользователь увидел платежную форму, необходимо запустить следующий код, размещённый в + файле ..Activity.java: + Double mntAmount = 12.00; + String mntPaymentSystem = "plastic"; + String mntOrderId = monetasdk.getOrderId(); + String mntCurrency = "RUB"; + WebView myWebView = (WebView) findViewById(R.id.webView); + monetasdk.showPaymentFrom(mntOrderId, mntAmount, mntCurrency, mntPaymentSystem, + myWebView, this); + Где + • mntOrderId - идентификатор операции, + • mntAmount - сумма операции, + • mntCurrency - валюта операции, + • mntPaymentSystem - способ оплаты. + Платежная кнопка через SDK PhoneGap + Скачайте и настройте SDK PhoneGap согласно инструкции. + Чтобы пользователь увидел платежную форму, необходимо запустить следующий код, размещённый в + файле index.html: + var mntOrderId, + mntAmount = 12, + mntCurrency, + mntPaymentSystem = 'plastic', + payFormElementId = 'payanyway', + payFormElementWidth = 333, + payFormElementHeight = 520; + monetaSdk.showPaymentFrom(mntOrderId, mntAmount, mntCurrency, mntPaymentSystem, + payFormElementId, payFormElementWidth, payFormElementHeight); + Где + • mntOrderId - идентификатор операции, + • mntAmount - сумма операции, + • mntCurrency - валюта операции, + • mntPaymentSystem - способ оплаты, + • payFormElementId - id блока, в который будет выводиться форма оплаты, + • payFormElementWidth - ширина формы оплаты, + • payFormElementHeight - высота формы оплаты. + | +PayAnyWay интеграция | 10 + Chapter + 2 + PayAnyWay интеграция + Topics: + • Интеграция PayAnyWay + общие сведения + • Интеграция с CMS + • Интеграция с PayAnyWay в + мобильном приложении + • Реализация Pay URL + • Преобразование платежной + формы в iframe + • Взаимодействие с + виджетом оплаты, + выводимым через iframe + • Проверка наличия товара + и его цены перед оплатой + (Check URL) + Интеграция PayAnyWay общие сведения + Интеграция оплаты товаров или услуг имеет общие принципы для всех возможных случаев, будь то + интеграция с веб-сайтом или интернет-магазином. В любом случае, можно выделить общую схему + взаимодействия с системой Moneta.ru: + Показать форму оплаты клиенту можно одним из трёх способов: 1) Перенаправив его на + MONETA.Assistant 2) Показав форму MONETA.Assistant в iframe 3) Запросить данные банковской + карты и осуществить списание через Merchant API (метод PaymentRequest) системы Moneta.ru (для этого + способа магазину потребуется сертификат PCI DSS). + Способы 1 и 2 были рассмотрены в главе 1, способ 3 будет рассмотрен в главе 6. + | +PayAnyWay интеграция | 11 + После успешной оплаты система Moneta.ru обращается к серверной части приложения партнера по + адресу, который указывается в настройках расширенного счёта в поле “Pay URL”. На указанный URL + система Moneta.ru оптавляет запрос со следующими параметрами: + MNT_ID + Идентификатор магазина в системе MONETA.RU. + MNT_TRANSACTION_ID + Внутренний идентификатор заказа, однозначно определяющий заказ в магазине. + MNT_OPERATION_ID + Номер операции в системе MONETA.RU. + MNT_AMOUNT + Фактическая сумма, полученная на оплату заказа, без учета комиссии за услуги системы «MONETA.RU». + MNT_CURRENCY_CODE + ISO код валюты, в которой произведена оплата заказа в магазине. + MNT_SUBSCRIBER_ID + Внутренний идентификатор пользователя, однозначно определяющий получателя в учетной системе + магазина. + MNT_TEST_MODE + Если оплата произведена в тестовом режиме, то параметр содержит «1», если в реальном – «0». + MNT_SIGNATURE + Код для идентификации отправителя и проверки целостности данных. + MNT_USER + Номер счета пользователя, если оплата производилась с пользовательского счета в системе + «MONETA.RU». + paymentSystem.unitId + Идентификатор платежной системы, если оплата производилась с платежной системы отличной от + «MONETA.RU». + MNT_CORRACCOUNT + Номер счета плательщика. Если оплата производилась с пользовательского счета в системе + «MONETA.RU», то совпадает с MNT_USER. + MNT_CUSTOM1, MNT_CUSTOM2, MNT_CUSTOM3, Другие параметры + Параметры, переданные в запросе на оплату через MONETA.Assistant + Параметры могут быть переданы методом POST или GET в зависимости от настроек расширенного счёта. + Первое что нужно сделать в скрипте “Pay URL” - проверить подпись MNT_SIGNATURE (то есть + достоверность запроса). + Запрос является достоверным, если присланное значение MNT_SIGNATURE идентично вычисленному + следующим образом: + MNT_SIGNATURE = MD5(MNT_ID + MNT_TRANSACTION_ID + MNT_OPERATION_ID + + MNT_AMOUNT + MNT_CURRENCY_CODE + MNT_SUBSCRIBER_ID + MNT_TEST_MODE + + КОД ПРОВЕРКИ ЦЕЛОСТНОСТИ ДАННЫХ) + Код проверки целостности данных - является свойством расширенного счёта и устанавливается в + личном кабинете в системе Moneta.ru. Данный код должен быть так же известен серверной стороне + приложения интернет-магазина, где реализуется скрипт Pay URL. + При формировании подписи, MNT_AMOUNT должно быть с двумя десятичными знаками, отделенными + точкой. + | +PayAnyWay интеграция | 12 + После проверки подлинности запроса, скрипт Pay URL может изменить статус заказа в интернет-магазине + на “Оплачен”, выполнить иные сопутствующие действия. + Скрипт должен отдать системе монета ответ со статусом 200 OK в заголовке и словом SUCCESS в случае + успеха, либо FAIL в случае ошибки в текстовом виде, без переносов. + Можно ответить на запрос Pay URL в XML формате (со статусом 200 OK): + + + + + + + + + + + + + + + + Где: + MNT_ID + Идентификатор магазина в системе MONETA.RU. + MNT_TRANSACTION_ID + Внутренний идентификатор заказа, однозначно определяющий заказ в магазине. + MNT_RESULT_CODE + Код ответа на запрос. + MNT_DESCRIPTION + Описание состояния заказа. + MNT_AMOUNT + Сумма оплаты. + MNT_SIGNATURE + Код для идентификации отправителя и проверки целостности данных. + MNT_ATTRIBUTES + Содержит произвольные параметры, которые будут сохранены в операции. + ATTRIBUTE + Элемент представляет один произвольный параметр операции. + KEY + Уникальное название параметра операции. Не должно превышать 32 символа. + VALUE + Значение параметра операции. + Ответ подписывается подписью MNT_SIGNATURE, которая формируется следующим образом: + MNT_SIGNATURE = MD5( MNT_RESULT_CODE + MNT_ID + MNT_TRANSACTION_ID + + КОД ПРОВЕРКИ ЦЕЛОСТНОСТИ ДАННЫХ ) + Как уже было отмечено выше, при формировании подписи, MNT_AMOUNT должно быть с двумя + десятичными знаками, отделенными точкой. + Коды ответа MNT_RESULT_CODE могут принимать следующие значения: +• 200 - заказ оплачен. Уведомление об оплате магазину доставлено, + | +PayAnyWay интеграция | 13 + • 100, 302, 402 - отправка уведомления об оплате должна быть и будет повторена, + • 500 - ошибка обработки. Автоматическая отправка уведомлений от системы Moneta.ru будет + остановлена. Необходимо связаться с группой поддержки MONETA.RU. + Важно отметить, что ответ системе Moneta.ru на запрос Pay URL должен быть сформирован с заголовком + 200 ОК, даже если не было принято никаких входящих параметров, поскольку при сохранении ссылки на + скрипт Pay URL в личном кабинете Moneta.ru, будет отправлен проверочный запрос без параметров, на + который ожидается ответ со статусом 200 ОК в заголовке. + Интеграция с CMS + Для многих CMS для интернет-магазинов уже реализованы модули для приема оплаты через PayAnyWay. + Модули работают по выше рассмотренной схеме, то есть подразумевают установку “Pay URL” в + настройках расширенного счёта в системе Moneta.ru. Скрипт “Pay URL” сменит статус заказа на + “Оплачен” в каждой конкретной CMS. + Большинство модулей PayAnyWay построено таким образом, чтобы ограничение выбора способов оплаты + включалось через настройки счёта в личном кабинете системы Moneta.ru (если такая необходимость + существует). + Интеграция с PayAnyWay в мобильном приложении + Приложения интернет-магазинов, как правило, разрабатываются по клиент-серверной архитектуре. + Каталог товаров хранится в backend и отображается на frontend. Заказы формируются на frontend и + отправляются в backend. + В соответствии с этим, на frontend отображается форма оплаты. Для этого можно использовать SDK, + рассмотренные в п.1.6.4 и 1.6.5. + Скрипт Pay URL следует разместить на backend. + Реализация Pay URL + Реализовать скрипт, обрабатывающий запрос об успешной оплате можно на любом языке + программирования, который позволяет реализовать веб-сервис. Общее описание алгоритма работы этого + скрипта приводится в п.2.1. + В SDK PHP есть метод, который позволяет реализовать скрипт Pay URL в нескольких строчках кода: + $monetaSDK = new Moneta\MonetaSdk(); + $result = $monetaSDK->processInputData(); + echo $result->render; + exit; + В результате работы данный код выводит SUCCESS или FAIL в зависимости от поступивших в + веб-сервис данных. + Преобразование платежной формы в iframe + В модулях PayAnyWay для CMS реализована стандартная для интернет-магазинов схема работы, в + которой после выбора способа оплаты выводится форма оплаты с кнопкой “Оплатить”, при нажатии на + которую происходит переход на форму ввода данных плательщика MONETA.Assistant. + На некоторых сайтах можно счесть возможным размещение вместо кнопки “Оплатить” платежной формы + MONETA.Assistant через iframe. + | +PayAnyWay интеграция | 14 + Для того, чтобы не меняя код платежного модуля и CMS преобразовать платежную форму в iframe надо + на страницу оформления заказа, либо на все страницы сайта поместить следующий Javascript-код: + + // remove paw payment button + $('#payment-form').remove(); + Где: + #payment-form - id формы оплаты, + .success - класс блока для вставки виджета оплаты. + Данный код можно использовать в CMS Webasyst без каких-либо доработок. При использовании в другой + CMS, следует внести изменения в части идентификаторов. + Взаимодействие с виджетом оплаты, выводимым через iframe + При использовании виджета оплаты, выводимого через iframe, вместо перехода на платежную форму + MONETA.Assistant, можно изменять визуальное отображение его элементов. Для этого .css файл с + изменениями необходимо прислать в техническую поддержку на review. + Примеры оформления платежной формы, выводимой через iframe можно посмотреть здесь (перейдите по + ссылке). + Особенности перехода на страницы Success, Fail URL при использовании + виджета в iframe + Платежная форма позволяет перенаправить покупателя на страницы магазина после успешной оплаты + (Success URL), после отмененной оплаты (Fail URL), после запроса на холдирование средств на карте + покупателя (InProgress URL), после добровольного отказа от оплаты (Return URL). Логично организовать + переход так, чтобы он происходил не внутри iframe, а в родительском окне. Для этого в настройках + расширенного счёта в личном кабинете Moneta.ru есть настройка “Target (для возврата iframe)”. В + рассматриваемом случае следует выбрать _parent или _top. + Некоторые CMS могут иметь встроенный Javascript-код, который блокирует переходы из дочернего (по + отношению к сайту интернет-магазина) iframe на указанный URL. Поэтому, если управление переходом + на Success и Fail URL работает некорректно, воспользуйтесь следующими рекомендациями: + 1) В качестве страницы Success URL создайте минимальную html страницу (без дизайна) с кодом: +

Спасибо за заказ!

+ Вернуться на сайт + | PayAnyWay интеграция | 15 + Создайте аналогичную страницу для Fail URL: +

Ошибка оплаты, попробуйте ещё раз.

+ Вернуться на сайт + 2) На страницу сайта, где выводится iframe с виджетом оплаты разместите следующий Javascript-код: + + 3) В личном кабинете системы Moneta.ru в настройках счёта укажите ссылки на страницы Success и Fail + URL. + Минималистичные страницы могут быть и пустыми и служить только для редиректа на полноценные + страницы в родительском окне. Т.е. Можно вызывать parent.callsuccess(); сразу после загрузки такой + страницы и ничего не выводить. В этом случае, страница Success URL будет выглядеть так: +
+ Динамические возможности Assistant.widget + Форма оплаты, выводимая через iframe, имеет встроенный Javascript-код, который позволяет + взаимодействовать с внешним Javascript-кодом родительского окна посредством интерфейса postMessage. + Для реализации обмена между окном iframe и родительским окном, в последнее нужно добавить + следующий код: + var regexp_simpleMessage = new RegExp('(^none|^success|^fail|^inprogress|^return)'); + // формат старых сообщений + function isJson(str) { + // проверка на формат JSON + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; + } + function listener(event) { + if (regexp_simpleMessage.test(event.data)) { + //проверяем соответствие сообщения старому "формату" + console.log('Parent, status: ' + event.data); + } + else { + if (isJson(event.data)){ + //проверка на соответствие сообщения формату JSON + var msg = JSON.parse(event.data); + switch (msg.m_type){ + //реакция на разные типы сообщений + case "widgetSize": + setBodyClass(); //в ответ на изменение размеров (обновление + assistant) устанавливаем css класс + var assistantDiv = document.getElementById('monetaAssistantDiv'); + if (msg.width < 320) { + msg.width = 320; + } + assistantDiv.style.height = msg.height; + assistantDiv.style.width = msg.width; + break; + case "status": + | PayAnyWay интеграция | 16 + console.log('Parent, status(JSON): ' + msg.status); + break; + } + } + } + } + if (window.addEventListener) { + window.addEventListener("message", listener, false); + } else { + window.attachEvent("onmessage", listener); + } + function setBodyClass() { + var msg = { + m_type: "bodyClass", + m_val: "test_class_name" + }; + var ifr = document.getElementById('pawassistantiframe').contentWindow; + ifr.postMessage(JSON.stringify(msg), '{url}'); + } + Примеры postMessage запросов. Сообщение, передаваемое через интерфейс postMessage текстовое и + представляет собой сериализованный JSON. + В каждом передаваемом объекте есть ключ m_type. На данный момент, возможные значения ключа + таковы: widgetSize, status, bodyClass. + Сообщения с типами widgetSize и status передаются родителю при изменении соответствующих + параметров. Так же данные сообщения с актуальными параметрами можно получить по запросу. + Примеры сообщений: + 1) {"m_type":"widgetSize","width":"300","height":"64"} + 2) {"m_type":"status","status":"none"} + Примеры запросов: + 1) Запрос статуса: {"m_type":"request","m_val":"status"} + 2) Запрос размера виджета: {"m_type":"request","m_val":"widgetSize"} + 3) Запрос на submit формы: {"m_type":"request","m_val":"submitForm"} + 4) Запрос на установку css класса для body виджета: + {"m_type":"bodyClass","m_val":"class_name"} + Ответы виджета: + 1) Класс добавлен: {"m_type":"bodyClass","result":"success","comment":""} + 2) Класс уже был, повторно не добавился: + {"m_type":"bodyClass","result":"error","comment":"Class already exist"} + 3) Недопустимые символы в названии класса (регексп названия класса: '([^A-Za-z0-9\ +\_])'): + {"m_type":"bodyClass","result":"error","comment":"Wrong class name"} + Проверка наличия товара и его цены перед оплатой (Check URL) + Интернет-магазину может понадобиться предварительная проверка параметров заказа перед оплатой. + Например, в случае продажи товаров, остатки которых могут быстро меняться, либо товаров, по которым + может меняться цена с течением времени. + В подобных случаях, можно использовать механизм проверочных запросов, которые система Moneta.ru + отправляет на URL, указанный в свойствах расширенного счёта в поле “Check URL”. + Проверочные запросы служат для того, чтобы: + • Убедиться, что заказ существует, что заказ еще не оплачен, что срок действия заказа не истек. + • Указать сумму заказа, если магазин ранее не передал сумму заказа в HTML-форме. +• Проверить статус заказа, если товар уже оплачен. + | +PayAnyWay интеграция | 17 + Проверочные запросы MONETA.Assistant отсылает на указанную страницу магазина Check URL в виде + HTTP запроса методом GET или POST. Ответ на HTTP запрос должен быть в формате XML. Во время + оплаты одного заказа MONETA.Assistant может несколько раз отсылать проверочные запросы. + В зависимости от ответа, который получит система Moneta.ru, возможны следующие варианты поведения + формы оплаты MONETA.Assistant: + • Меняется сумма заказа, + • Нормальное продолжение оплаты заказа, + • Выводится сообщение об ошибке, прием оплаты прекращается. + В запросе на Check URL от системы Moneta.ru передаются следующие GET или POST параметры: + MNT_COMMAND CHECK + Установлен в значение CHECK для проверочных запросов. + MNT_ID + Идентификатор магазина в системе MONETA.RU. + MNT_TRANSACTION_ID + Внутренний идентификатор заказа, однозначно определяющий заказ в магазине. + MNT_OPERATION_ID + Номер операции в системе MONETA.RU. Если операция еще не создана в системе MONETA.RU, то это + поле не будет отправлено. + MNT_AMOUNT + Фактическая сумма, полученная на оплату заказа, без учета комиссии за услуги системы MONETA.RU. + Если магазин не передал сумму заказа в HTML форме, то данного параметра может не быть. Если в + запросе нет данного параметра, то магазин обязан вернуть сумму заказа в ответе на данный запрос, иначе + MONETA.Assistant завершит обработку с ошибкой. + MNT_CURRENCY_CODE + ISO код валюты + MNT_SUBSCRIBER_ID + Внутренний идентификатор пользователя. + MNT_TEST_MODE + В тестовом режиме - «1», если в реальном – «0». + MNT_SIGNATURE + Код для идентификации отправителя и проверки целостности данных. + MNT_USER + Номер счета пользователя, если пользователь выбрал оплату с пользовательского счета в системе + «MONETA.RU». Данного параметра может не быть. + paymentSystem.unitId + Идентификатор платежной системы. Данного параметра может не быть. + MNT_CORRACCOUNT + Номер счета плательщика. Если оплата производилась с пользовательского счета в системе + «MONETA.RU», то совпадает с MNT_USER. Данного параметра может не быть. + MNT_CUSTOM1, MNT_CUSTOM2, MNT_CUSTOM3, + Другие параметры, переданные в запросе на оплату через MONETA.Assistant. + | +PayAnyWay интеграция | 18 + В параметре MNT_SIGNATURE передается подпись проверочного запроса, которая формируется + следующим образом: + MNT_SIGNATURE = MD5( MNT_COMMAND + MNT_ID + MNT_TRANSACTION_ID + MNT_OPERATION_ID + + MNT_AMOUNT + MNT_CURRENCY_CODE + MNT_SUBSCRIBER_ID + MNT_TEST_MODE + + КОД ПРОВЕРКИ ЦЕЛОСТНОСТИ ДАННЫХ ) + Формат ответа должен быть следующим: + + + + + + + + + + + + + + + + Параметры ответа: + MNT_ID + Номер расширенного счёта магазина в системе MONETA.RU. + MNT_TRANSACTION_ID + Внутренний идентификатор заказа. + MNT_RESULT_CODE + Код ответа на запрос. + MNT_DESCRIPTION + Описание состояния заказа. + MNT_AMOUNT + Сумма оплаты. + MNT_SIGNATURE + Код для идентификации отправителя и проверки целостности данных. + MNT_ATTRIBUTES + Содержит произвольные параметры, которые будут сохранены в операции. Необязательный элемент. + ATTRIBUTE + Элемент представляет один произвольный параметр операции. + KEY + Уникальное название параметра операции. Не должно превышать 32 символа. + VALUE + Значение параметра операции. + В ответе на проверочный запрос подпись формируется следующим образом: + MNT_SIGNATURE = MD5(MNT_RESULT_CODE + MNT_ID + MNT_TRANSACTION_ID + + КОД ПРОВЕРКИ ЦЕЛОСТНОСТИ ДАННЫХ) + При формировании подписи, MNT_AMOUNT должно быть с двумя десятичными знаками, отделенными + точкой. + | +PayAnyWay интеграция | 19 + При помощи параметра MNT_RESULT_CODE меняется поведение формы оплаты. Наиболее частые + варианты ответа такие: + • MNT_RESULT_CODE = 100, MNT_AMOUNT - установлено в некоторое значение. В этом случае + меняется сумма к оплате текущего заказа. Изначально, сумма могла быть не указана вовсе. + • MNT_RESULT_CODE = 402, MNT_AMOUNT - не принимается во внимание. В таком случае оплата + будет продолжена в штатном режиме. + • MNT_RESULT_CODE = 500, MNT_AMOUNT - не принимается во внимание. Будет выведено + сообщение MNT_DESCRIPTION в качестве сообщения об ошибке, оплата не будет продолжена. + Следует учитывать, что система Moneta.ru может отправлять несколько проверочных запросов на Check + URL. После отправки ответа с MNT_RESULT_CODE равным 100, в ответе на следующий запрос + MNT_RESULT_CODE должен быть 402, либо 500, иначе MONETA.Assistant выведет ошибку и прервет + оплату. + Список всех возможных значений MNT_RESULT_CODE: + • 100 - ответ содержит сумму заказа для оплаты. Данным кодом следует отвечать, когда в параметрах + проверочного запроса не был указан параметр MNT_AMOUNT, либо сумма заказа могла измениться. + • 200 - заказ уже был оплачен. Уведомление об оплате магазину доставлено. + • 302 - заказ находится в обработке. Точный статус оплаты заказа определить невозможно. В этом + случае в форме оплаты будет выведено MNT_DESCRIPTION в качестве сообщения об ошибке, + оплата не будет продолжена. + • 402 - заказ создан и готов к оплате. Уведомление об оплате магазину не доставлено. + • 500 - заказ не является актуальным в магазине (например, заказ отменен). + | +PayAnyWay выписка счёта | 20 + Chapter + 3 + PayAnyWay выписка счёта + Topics: + • Создание счёта через + Merchant API + Система Moneta.ru позволяет партнерам работать, выставляя их + покупателям счёта на оплату. Для работы по такой схеме удобнее + открывать отдельный расширенный счёт в системе Moneta.ru, с + которого и будут выписываться счета на оплату. + В настройках счёта в личном кабинете Moneta.ru нужно очистить поля + Check URL и Pay URL, в поле “Тип интерфейса” нужно указать “Нет”. + Для создания нового счёта на оплату перейдите в раздел “Рабочий + кабинет” и далее к опции “Счёт к оплате”. + После создания счёта в системе Moneta.ru будет создана платежная + операция, на e-mail покупате будет отправлено письмо со ссылкой + на оплату данной операции. Поле “Номер счёта плательщика” - не + обязательное. Если оставить его пустым, то плательщику будут + доступны все возможные способы оплаты на выбор. + Создание счёта через Merchant API + Выписку счетов на оплату можно автоматизировать. В этом случае, скрипт Pay URL будет полезен. + Для создания счёта на оплату в Merchant API есть метод InvoiceRequest. В качестве аргумента в метод + передается объект с типом InvoiceRequestType, который имеет следующие атрибуты: + payer + Номер счета плательщика. Необязательный элемент. Тип: string + payee + Номер счета получателя. + amount + Сумма. Необязательный элемент. + clientTransaction + Внешний номер операции. + description + Описание операции. Необязательный элемент. + mnt_custom1, mnt_custom2, mnt_custom3, ... + Произвольные параметры. + operationInfo + Набор полей, которые необходимо сохранить в качестве атрибутов операции. Значения дат в формате + dd.MM.yyyy HH:mm:ss. Для того чтобы провести прямое дебетование, следует передать атрибут с ключом + "SUBSCRIBERID" и в значении указать ID подписчика. Прямое дебетование будет проведено, если ID + подписчика указано верно, если пользователь разрешил проводить прямое дебетование, если указанная + сумма есть на балансе пользователя. В данном случае деньги будут списаны со счета пользователя, в + ответе на запрос в поле Статус будет указано INPROGRESS, зачисление денег на счет получателя будет + | +PayAnyWay выписка счёта | 21 + проведено в асинхронном режиме. Если прямое дебетование не может быть проведено или во время его + выполнения произойдет ошибка, то инвойс будет создан и в ответе на запрос в поле Статус будет указано + CREATED. USERCONTACT - ссылка на оплату счета будет отправлена в письме или SMS (можно + перечислить получателей через запятую). Необязательный элемент. + Метод InvoiceRequest возвращает ID созданного счёта на оплату. Для оплаты счёта нужно перенаправить + пользователя по ссылке: + https://www.payanyway.ru/assistant.htm?operationId=ID_созданного_счёта + В данную ссылку можно передавать так же параметры MONETA.Assistant, которые рассмотрены в Главе + 1. + Можно поместить данную форму в iframe, заменив .htm на .widget, можно разместить ссылку в + социальной сети. + Поскольку merchant API подразумевает автоматизацию генерации ссылок на оплату счёта, целесообразно + так же настроить скрипт Pay URL, который будет вызван системой Монета.ру после успешного + зачисления средств на расширенный счёт интернет-магазина. + Пример кода на SDK php + $monetaSDK = new Moneta\MonetaSdk(); + $monetaSDK->checkMonetaServiceConnection(); + $operationId = $monetaSDK->sdkMonetaCreateInvoice($payer = null, $payee, $amount, + $orderId, $paymentSystem = 'payanyway', $isRegular = false, $additionalData = null); + Пример кода на SDK C# + MonetaSDK monetaSDK = new MonetaSDK(); + MonetaSdkResult result = monetaSDK.sdkMonetaCreateInvoice(string payer, long payee, + decimal amount, string clientTransaction, bool isRegular); + | +PayAnyWay с холдированием | 22 + Chapter + 4 + PayAnyWay с холдированием + Topics: + • Пример кода на SDK php + У некоторых интернет-магазинов остатки товаров, представленных + на сайте могут быть ограниченными. В таком случае, важно + принимать оплату с покупателя лишь в том случае, если товар + или услуга действительно могут быть ему предоставлены. Если у + магазина есть учетная система, которая синхронизирует остатки + с интернет-магазином, то целесообразно реализовать Check URL + как описано в п.2.7. Если же остатки товара уточняются в ручном + режиме, то можно использовать оплату с холдированием. При этом + деньги с банковской карты покупателя не списываются сразу же + в момент оплаты товара, а блокируются на карте покупателя до + принятия решения об отгрузке интернет-магазином, который после + проверки остатков принимает решение о списании денег в свою + пользу, либо об отмене операции оплаты. При отмене операции, + ранее блокированная на карте покупателя сумма разблокируется. + Механизм приема платежей с холдированием выгодно использовать + интернет-магазину с плавающими остатками, чтобы не терять на + комиссии, которая оплачивается с каждой успешно завершенной + операции оплаты. + Последовательность действий, при приеме платежей с холдированием + такова: + 1. Создать счёт при помощи merchant API (метод InvoiceRequest) + на оплату способом “банковские карты” с атрибутом + AUTHORIZEONLY установленом в 1 (еденину). + 2. Переадресовать покупателя на платежную форму + MONETA.Assistant с параметром operationId, который был получен + на предыдущем шаге, для оплаты счёта: + https://www.payanyway.ru/assistant.htm? + operationId=ID_созданного_счёта + Параметры выставленного счёта, а также ID операции operationId, + сохранить в базу данных интернет-магазина. + 3. В заранее подготовленном скрипте, принять URL уведомление + от системы Moneta.ru “Вызвать URL после авторизации средств”, + в этом скрипте по параметру MNT_OPERATION_ID отметить + запись в базе данных по строке с operationId как активную + (готовую к обработке). По активной операции деньги холдированы + (зарезервированы) на банковской карте покупателя. + 4. Если интернет-магазин убедился что может поставить нужный + покупателю товар, то он (магазин) подтверждает операцию с ID + равным operationId на сумму равную или меньше чем указанная + ранее (при холдировании). Подтвердить операцию можно через + merchant API (метод ConfirmTransactionRequest), или вручную + в личном кабинете системы Moneta.ru. После подтверждения + операции система Moneta.ru отправит URL уведомление на Pay + URL. + | PayAnyWay с холдированием | 23 + 5. Если товара нет в наличии, либо существуют иные причины, по + которым магазин не может выполнить заказ, магазин Отменяет + операцию с ID равным operationId. Отменить операцию можно + через merchant API (метод CancelTransactionRequest), или вручную + в личном кабинете системы Moneta.ru. + Покупатель имеет право отменить блокировку своих средств на + банковской карте, написав заявление на отмену блокировки в банк. + Попытка подтвердить не оплаченную ранее операцию приведёт к её + заморозке в системе Moneta.ru. + URL уведомление “Вызвать URL после авторизации средств” + приходит с параметрами аналогичными тем что описаны выше + для Check URL (в разделе 2.7), за исключения MNT_COMMAND + CHECK. Настройка ссылки на скрипт, принимающий уведомление + производится в личном кабинете системы Moneta.ru в пункте меню + "Действия при зачислении/списании средств". + При отмене операции с холдированием, комиссия за проведение + операции не взимается. Комиссия возникает лишь при подтверждении + операции. Это делает платежи с холдированием более выгодными + чем оплаты и возвраты средств для проектов с подвижными или + неопределенными остатками. + Пример кода на SDK php + Создать счёт при помощи Merchant API (метод InvoiceRequest) на оплату способом “банковские карты” с + атрибутом AUTHORIZEONLY установленом в 1 (еденину). + $monetaSDK = new Moneta\MonetaSdk(); + $monetaSDK->checkMonetaServiceConnection(); + $additionalData = array('AUTHORIZEONLY' => '1'); + $operation_id = $monetaSDK->sdkMonetaCreateInvoice(null, $settings->MNT_ID, $amount, + $transaction_id, 'payanyway', false, $additionalData); + Подтвердить операцию с ID равным operationId (метод ConfirmTransactionRequest): + $monetaSDK = new Moneta\MonetaSdk(); + $monetaSDK->checkMonetaServiceConnection(); + try { + $confirmRequest = new \Moneta\Types\ConfirmTransactionRequest(); + $confirmRequest->transactionId = $operationId; + $confirmRequest->amount = number_format($amount, 2, '.', ''); + $operationInfo = $monetaSDK->ConfirmTransaction($confirmRequest); + $result = json_decode(json_encode($operationInfo, true)); + // отметка завершения операции в БД магазина + // ... + } + catch (\Exception $e) { + // 'ERROR: Проведение операции не удалась, сделайте это в личном кабинете Moneta.ru' + } + Отменить операцию (метод CancelTransactionRequest): + $monetaSDK = new Moneta\MonetaSdk(); + $monetaSDK->checkMonetaServiceConnection(); + try { + $confirmRequest = new \Moneta\Types\CancelTransactionRequest(); + $confirmRequest->transactionId = $operationId; + $operationInfo = $monetaSDK->CancelTransaction($confirmRequest); + $result = json_decode(json_encode($operationInfo, true)); + | +PayAnyWay с холдированием | 24 + if (is_object($result) && is_object($result->data) && isset($result->data +>operationStatus) && $result->data->operationStatus == 'CANCELED') { + // +всё ОК + // + // +Операция {$model->operation_id} отменена + отметить операцию в БД магазина + // ... + } + } + else { + // 'ERROR: Отмена не удалась, сделайте отмену операции в личном кабинете + Moneta.ru' + } + } + catch (\Exception $e) { + // +непредвиденная ошибка + $message = 'ERROR: ' . print_r($e, true); +Chapter + 5 + PayAnyWay регулярные платежи + Topics: + • Реализация регулярных + платежей на SDK PHP + В проектах, предоставляющих услуги или товары на регулярной + основе (ежемесячно, ежегодно, ежедневно) удобным способом + списания денег у покупателей является рекурентный (регулярный) + платеж. При этом сумма списания может быть постоянной, либо + может изменяться. + Для реализации такой схемы работы в системе Монета.ру есть + специальные методы в Merchant API. Последовательность действий, + выполняемая магазином для реализации данной схемы, будет такой: + 1. Создать счёт при помощи Merchant API (метод InvoiceRequest) + на оплату способом “банковские карты” с атрибутом + PAYMENTTOKEN в значении 'request' (строка). + 2. Перенаправить покупателя на форму MONETA.Assistant с + параметром operationId для оплаты счёта: + https://www.payanyway.ru/assistant.htm? + operationId=ID_созданного_счёта + Значение operationId получено на предыдущем шаге. + На платежной форме для операций с установленным + PAYMENTTOKEN=request будет отображена дополнительная + галочка "Запомнить карту", требующая явного согласия + пользователя. Если пользователь согласия не даст, то + операция будет обработана как обычный платеж и параметр + PAYMENTTOKEN будет записан как refused. + 3. Получить URL уведомление на Pay URL после успешной оплаты + выставленного счёта. Значение параметра paymenttoken (если + оно не равно refused или request) сохранить для дальнейшего + использования. Сохранить дату осуществления платежа. + Поставить данного покупателя в расписание на регулярное + (ежемесячное, ежедневное, еженедельное и т.д.) списание средств. + 4. За несколько дней до регулярного списания отправить покупателю + сообщение на e-mail с напоминанием о предстоящем регулярном + списании средств и ссылкой, по которой покупатель сможет + исключить себя из расписания на регулярное списание средств. + 5. При переходе по ссылке на прекращение регулярных списаний, + исключить покупателя из расписание на регулярное списание + средств. + 6. Согласно установленному расписанию, произвести списание + средств с покупателя, при помощи ранее полученного значения + paymenttoken. Для этого нужно выполнить метод PaymentRequest + через Merchant API, передав параметр paymenttoken и его + значение как дополнительный атрибут объекта OperationInfo. + Сумма операции устанавливается произвольной (по соглашению + сторон). + 7. Если в ответ на запрос PaymentRequest получена ошибка, нужно + исключить покупателя из расписания на регулярное списание + | +PayAnyWay регулярные платежи | 26 + средств и заново произвести платеж как было описано в пунктах + 1-3. + 8. Повторять шаги 4-7 с заданной регулярностью. + Проверять ответ на запрос PaymentRequest необходимо, поскольку + покупатель может обратиться в его банк для прекращения регулярных + списаний с его карты. Если это произошло, реализуются действия, + описанные в шаге 7. + Для того, чтобы осуществлять рекурентные платежи, в системе + монета необходимо наделить счёт, принимающего платежи, + интернет-магазина специальными дополнительными правами. + Рассмотренная схема может быть использована так же для реализации + платежей без повторного ввода данных банковской карты покупателя. + В этом случае, запрос PaymentRequest с параметром paymenttoken + и его значением можно отправить не по расписанию, а по нажатию + кнопки “Оплатить” покупателем в любой момент времени. Схема + работы при списании средств без повторного ввода данных будет + выглядеть так: + 1. Создать счёт при помощи Merchant API (метод InvoiceRequest) + на оплату способом “банковские карты” с атрибутом + PAYMENTTOKEN в значении 'request' (строка). + 2. Перенаправить покупателя на форму MONETA.Assistant с + параметром operationId для оплаты счёта: + https://www.payanyway.ru/assistant.htm? + operationId=ID_созданного_счёта + Значение operationId получено на предыдущем шаге. + 3. Получить URL уведомление на Pay URL после успешной + оплаты выставленного счёта. Значение параметра paymenttoken + сохранить для дальнейшего использования. + 4. Произвести списание средств с покупателя, при помощи ранее + полученного значения paymenttoken. Для этого нужно выполнить + метод PaymentRequest через Merchant API, передав параметр + paymenttoken и его значение как дополнительный атрибут объекта + OperationInfo. Сумма операции устанавливается произвольной (по + соглашению сторон). + 5. Если в ответ на запрос PaymentRequest получена ошибка, нужно + заново произвести платеж как было описано в пунктах 1-3. + 6. Повторять шаги 4-5 при нажатии покупателем на кнопку + “Оплатить” без повторного ввода данных банковской карты. + Реализация регулярных платежей на SDK PHP + Перед началом работы + Для того, чтобы организовать работу сервиса с приемом регулярных платежей, Вам понадобится + подключение к источнику данных. Для подключения нужно внести настройки для Вашего проекта в файл + data_storage.ini, параметр monetasdk_storage_type может принимать значение: files или mysql + Если установленное значение - пустая строка, сохранение данных в локальное хранилище не будет + производиться. + Генерация формы рекурентного платежа + Чтобы вывести форму рекурентного платежа, можно использовать следующий код: + $monetaSDK = new Moneta\MonetaSdk(); + | +PayAnyWay регулярные платежи | 27 + $monetaSDK->processCleanChoosenPaymentSystem(); + $result = $monetaSDK->showPaymentFrom(null, 4, 'RUB', 'Рекурентный платеж', false, + 'plastic', true); + echo $result->render; + Аргумент метода $isRegular устанавливается в true, поэтому отображенная данным кодом форма будет + готова к приему рекурентных платежей. + Генерация формы рекурентного платежа + Код обработчика будет таким: + $monetaSDK = new Moneta\MonetaSdk(); + $result = $monetaSDK->processInputData(); + echo $result->render; + При поступлении подтверждения об успешном рекурентном платеже, обработчик выполнит все + необходимые для поддержания регулярных платежей действия, а именно - сохранит платежный токен в + хранилище данных. + Рассылка уведомлений о предстоящем платеже + Для рассылки уведомлений о предстоящем регулярном платеже со ссылкой на скрипт отмены, добавьте + следующий код в расписание для запуска не менее чем каждый день: + $monetaSDK = new Moneta\MonetaSdk(); + $monetaSDK->processRecurentPaymentNotificationCronTask(); + Код отправит на e-mail уведомления, сформированные из view RegularNotification.php. Настройки + уведомлений находятся в файле regular_payments.ini + Осуществление регулярных платежей + Для осуществления регулярных ежемесячных платежей добавьте следующий код в расписание для + запуска не реже чем каждый день: + $monetaSDK = new Moneta\MonetaSdk(); + $monetaSDK->processRecurentPaymentTransferCronTask(); + Обработчик отмены регулярного платежа + Ранее рассматривался конфигурационный файл regular_payments.ini, в котором есть параметр + regular_payments_cancel_url. По указанному адресу нужно разместить следующий код: + $monetaSDK = new Moneta\MonetaSdk(); + $result = $monetaSDK->processInputData(); + Данный код отменит регулярные платежи по ссылке из письма, которое отправлялось ранее. + Ручное проведение регулярного платежа + Имея номер успешно проведённой операции, можно провести повторный (регулярный) платеж в ручном + режиме следующим методом: + $monetaSDK = new Moneta\MonetaSdk(); + $result = $monetaSDK->processPayRecurrent($operationId, $description); + где $operationId - идентификатор успешно проведенной ранее операции, + $description - описание для новой (регулярной) операции. + | +PayAnyWay выплаты и списания | 28 + Chapter + 6 + PayAnyWay выплаты и списания + Topics: + • Выплаты через Merchant + API + • Выплаты на банковскую + карту + • Списание с карты и + выплаты для проектов с + PCI DSS + Выплаты через Merchant API + Метод Payment можно использовать для вывода средств в различные платежные системы. Для выбора + платежной системы в методе Payment требуется указать нужный элемент payee. Так же, в элементе + operationInfo, передаются параметры необходимые для вывода средств, которые зависят от выбранной + платежной системы. + Для вывода средств со счета Moneta.Ru выполните следующие шаги: + 1. Рекомендуется (но необязательно) сначала выполнить запрос VerifyPayment, чтобы проверить + параметры платежа. + 2. Если плательщик получает платежный пароль в SMS, выполните метод + GetAccountPaymentPasswordChallenge для получения плательщиком платежного пароля. + 3. Выполните метод Payment. Если операция проведена успешно, то ответ будет содержать атрибут + statusid со значением SUCCEED. + Следующая таблица показывает значения элемента payee и возможные значения элемента operationInfo, + которые нужно передать для вывода средств со счета системы Moneta.Ru в различные платежные + системы: + Платежная + система + Получатель + (payee) + Значения в OperationInfo + Яндекс.Деньги + 13 + YANDEXACCOUNT Номер кошелька в системе Яндекс.Деньги. + VKontakte + WebMoney + CyberPlat + Банковский + перевод + 139 + 2,3,4 + 1144 + 5 + VKONTAKTEID E-mail или имя пользователя ВКонтакте. + WEBMONEYWMID Идентификатор пользователя (WMID). + WEBMONEYPURSE Номер кошелька WMR (если payee = 2). + Номер кошелька WMZ (если payee = 3). Номер кошелька WME + (если payee = 4). + CYBERPLATPROVIDERID Значение + CYBERPLATPROVIDERID. CUSTOMFIELD:100 + WIREUSERNAME Наименование получателя. WIREUSERINN + ИНН получателя. WIREPAYMENTPURPOSE Назначение + платежа. WIREBANKACCOUNT Номер расчетного счета. + | +PayAnyWay выплаты и списания | 29 + Платежная + система + Банковские + карты (через + Банк Русский + Стандарт) + Только для + проектов с PCI + DSS + Банковские + карты (через + Альфа-Банк) + Только для + проектов с PCI + DSS + Получатель + (payee) + Значения в OperationInfo + WIREBANKBIK БИК банка. WIREBANKNAME Наименование + банка. WIREBANKKS Корр. счет банка. WIREKPP КПП. + WIREKBK КБК. WIREOKTMO ОКТМО. WIREDOCINDEX + УИН (индекс документа). + 275 + 279 + Деньги@Mail.ru 268 + QIWI + 255 + PAYERCOUNTRY Код страны. Возможные значения: RUS + (перевод средств на карту, выпущенную российским банком). + PAYEECARDNUMBER Полный номер карты. + PAYEECARDNUMBER Полный номер карты. + CARDEXPIRATION Срок действия карты в формате: MM/YYYY + CUSTOMFIELD:RECIPIENT E-mail или 16-ти + значный номер пользователя в Деньги@Mail.ru + CUSTOMFIELD:PAYMENT_PURPOSE Назначение платежа. + EXTERNALACCOUNTID QIWI account. + Параметры для вывода средств в методе Payment зависят от платежной системы, куда выводятся средства. + Чтобы их получить: + 1. Сделайте вывод небольшой суммы со счета в Moneta.Ru в нужную платежную систему через + интерфейс пользователя для получения номера операции. + 2. Используйте метод GetOperationDetailsById для получения деталей операции. + 3. В ответе получите элемент OperationInfo. Этот элемент содержит все параметры необходимые для + вывода средств через Merchant API. + Теперь Вы можете отредактировать элемент OperationInfo и использовать результат в методе Payment для + вывода средств. + Выплаты на банковскую карту + Если проект имеет сертификат PCI DSS хотя бы в минимальном исполнении, либо не имеет сертификат + PCI DSS, то пользователя можно отправить на страницу https://www.moneta.ru/secureCardData.htm + в системе Монета.ру для ввода данных банковской карты и получения токена карты. Для этого + формируется запрос на форму https://www.moneta.ru/secureCardData.htm со следующими параметрами: +
+ + + + + + + + | PayAnyWay выплаты и списания | 30 + + + +
+ То же самое в виде ссылки: + https://www.moneta.ru/secureCardData.htm?publicId=d21ba7f4-1272-43c9-bbc5 +e8c7cb75789e&MNT_ID=12345678&MNT_TRANSACTION_ID=%D0%B2%D0%BD%D0%B5%D1%88%D0%BD + %D0%B8%D0%B9+%D0%B8%D0%B4%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%82%D0%BE + %D1%80+%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B8&redirectUrl=https%3A%2F%2F + %D1%81%D0%B0%D0%B9%D1%82.ru + %2Fmoneta_callback.php&MNT_SIGNATURE=289b0373ec7d25dff0f25534e21adc27&secure + %5BPAYEECARDNUMBER%5D=required&secure%5BCARDEXPIRATION%5D=required&secure + %5BCARDCVV2%5D=required&formTarget=_top&MNT_DESCRIPTION=%D0%9E%D0%BF + %D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5+%D0%BF%D0%BB%D0%B0%D1%82%D0%B5%D0%B6%D0%B0 + Успешный ответ придёт на redirectUrl: + redirectUrl?secureToken=K5JelWrviLGSqhff-1406-05f77bc082ce3c0cd242dd70613e7518ab7... + &expirationDate=2015-08-13T09:08:13.381+03:00&MNT_SIGNATURE=bc6e91a851af8458dd8cf... + &MNT_ID=12345678&MNT_TRANSACTION_ID=внешний идентификатор операции&key=value + Где: + MNT_SIGNATURE = md5(MNT_ID+MNT_TRANSACTION_ID+secureToken+Код_проверки_на_счете_MNT_ID) + Ответ с ошибкой будет таким: + redirectUrl?faultstring=Пользователь не найден&faultDetail=500.1.14&MNT_ID=32841945 + &MNT_TRANSACTION_ID=123&key=value + Либо, если мы смогли вычислить MNT_SIGNATURE, то: + redirectUrl?faultstring=Подпись на форме оплаты обязательна&faultDetail=500.3.2.56.2 + &MNT_SIGNATURE=d390e5dc6ed4ae227e452473519fbe92&MNT_ID=12345678 + &MNT_TRANSACTION_ID=внешний идентификатор операции&key=value + Где: + MNT_SIGNATURE = md5(MNT_ID+MNT_TRANSACTION_ID+faultDetail+Код_проверки_на_счете_MNT_ID) + Далее используем secureToken, полученный в ответе. + Перевод денег на карту с использованием SecureToken (JSON): + {"Envelope": { + "Header": { + "Security": { + "UsernameToken": { + "Username": "USERNAME", + "Password": "PASSWORD" + } + } + }, + "Body": { + "PaymentRequest": { + "version": "VERSION_2", + "payer": "СЧЕТ ПЛАТЕЛЬЩИКА", + "paymentPassword": "ПЛАТЕЖНЫЙ ПАРОЛЬ СЧЕТА ПЛАТЕЛЬЩИКА", + "payee": "279", + "amount": СУММА, + "isPayerAmount": true, + "clientTransaction": "ВАШ ID ОПЕРАЦИИ", + "operationInfo": { + | PayAnyWay выплаты и списания | 31 + "attribute": [ + { + "key": "SECURETOKEN", + "value": + "jz8QdLCXQTeWXHNS-1075-788270e2fcf170c60136a2a2ee0248ff85f7cac" + } + ] + } + } + } + }} + Списание денег с карты с использованием SecureToken (JSON): + {"Envelope": { + "Header": { + "Security": { + "UsernameToken": { + "Username": "USERNAME", + "Password": "PASSWORD" + } + } + }, + "Body": { + "PaymentRequest": { + "version": "VERSION_2", + "payer": "303", + "payee": "СЧЕТ ПОЛУЧАТЕЛЯ", + "amount": СУММА, + "isPayerAmount": false, + "clientTransaction": "ВАШ ID ОПЕРАЦИИ", + "operationInfo": { + "attribute": [ + { + "key": "SECURETOKEN", + "value": + "jz8QdLCXQTeWXHNS-1075-788270e2fcf170c60136a2a2ee0248ff85f7cac" + } + ] + } + } + } + }} + Списание с карты и выплаты для проектов с PCI DSS + Сертификация PCI DSS позволяет проекту осуществлять ввод данных банковских карт на своей стороне. + Временное сохранение данных карты (в мобильном приложении) + Чтобы сохранить данные карты в системе Moneta.ru с возможностью затем осуществить списание + или выплату на эту карту, используется метод SecureDataRequest, который работает только для JSON + протокола (для SOAP не работает) - запрос должен быть отправлен на url: https://www.moneta.ru/services/ + publiс + { + "SecureDataRequest": { + "publicId": "d21ba7f4-1272-43c9-bbc5-e8c7cb75789e", + "attribute": [ + { + "key": "CARDNUMBER", + "value": "1234567890123456" + }, + { + "key": "CARDEXPIRATION", + "value": "01\/2020" + | +PayAnyWay выплаты и списания | 32 + }, + { + } + ] + } + } + "key": "CARDCVV2", + "value": "123" + Данные сохраняются на 15 минут. + Ответ на запрос будет таким (JSON): + { + } + "SecureDataResponse": { + "expirationDate": "2015-08-10T09:35:02.168+03:00", + "secureToken": "jz8QdLCXQTeWXHNS-1075-788270e2fcf170c60136a2a2ee0248ff85f7cac" + } + Запрос можно сделать с помощью ajax с html страницы (пример с использованием jquery), а так же и без + помощи ajax. За дополнительной технической информацией по этому поводу обратитесь в техническую + поддержку. + Имея secureToken можно осуществить списание денег с карты и перевод денег на карту. За + дополнительной технической информацией по этому поводу обратитесь в техническую поддержку. + Списание с банковской карты через Merchant API + Запрос методом PaymentRequest можно отправить с данными банковской карты. Если карта не имеет + защиты 3DS, то платеж будет сразу же проведён. Если же 3DS у данной карты включен, то в ответе на + запрос PaymentRequest придёт ссылка на страницу ввода кода 3DS. За дополнительной технической + информацией по этому поводу обратитесь в техническую поддержку. +Chapter + 7 + PayAnyWay площадкам + Банковским организациям, желающим предоставлять их клиентам + услуги эквайринга, в том числе в концепции White label, можно + использовать функционал PayAnyWay для площадок. В этом + случае станет возможным открывать аналитические счета в системе + Moneta.ru для каждого клиента организации (банка), а так же + принимать online платежи на такие счета. За дополнительной + технической информацией по этому поводу обратитесь в техническую + поддержку. + | +Полезные ссылки | 34 + Chapter + 8 + Полезные ссылки + Topics: + • SDK на различных языках + • Документация по + MONETA.Assistant + • Описание методов Merchant + API + SDK на различных языках + • C# + • PHP + • Python3 + • Ruby + • Java + Документация по MONETA.Assistant + • https://www.moneta.ru/doc/MONETA.Assistant.ru.pdf + Описание методов Merchant API + • https://www.moneta.ru/doc/MONETA.MerchantAPI.v2.ru.pdf \ No newline at end of file diff --git a/RAILWAY_DEPLOY.md b/RAILWAY_DEPLOY.md new file mode 100644 index 000000000..851202dc0 --- /dev/null +++ b/RAILWAY_DEPLOY.md @@ -0,0 +1,107 @@ +# Инструкции РїРѕ деплою РЅР° Railway + +## Что такое Railway? +Railway - это платформа для деплоя приложений, которая автоматически обнаруживает тип вашего проекта Рё настраивает его для продакшена. + +## Шаги для деплоя: + +### 1. Подготовка +- Убедитесь, что ваш РєРѕРґ загружен РІ GitHub репозиторий +- РЈ вас есть API ключи для E2B Рё Firecrawl +- Опционально: API ключи для ИИ провайдеров (Anthropic, OpenAI, Gemini, Groq) + +### 2. Создание проекта РЅР° Railway +1. Перейти РЅР° https://railway.app/ +2. Зарегистрироваться/войти через GitHub +3. Нажать "New Project" +4. Выбрать "Deploy from GitHub repo" +5. Выбрать ваш репозиторий СЃ Open Lovable + +### 3. Настройка переменных окружения +Р’ разделе "Variables" добавить следующие переменные: + +**Обязательные:** +- `E2B_API_KEY` - ваш ключ РѕС‚ E2B (https://e2b.dev) +- `FIRECRAWL_API_KEY` - ваш ключ РѕС‚ Firecrawl (https://firecrawl.dev) + +**ИИ провайдеры (нужен РјРёРЅРёРјСѓРј РѕРґРёРЅ):** +- `ANTHROPIC_API_KEY` - ключ Anthropic +- `OPENAI_API_KEY` - ключ OpenAI +- `GEMINI_API_KEY` - ключ Google Gemini +- `GROQ_API_KEY` - ключ Groq + +**Системные (добавляются автоматически):** +- `NODE_ENV=production` +- `PORT` - устанавливается Railway автоматически + +### 4. Деплой +Railway автоматически: +- Установит зависимости (`npm install`) +- Соберёт проект (`npm run build`) +- Запустит сервер (`npm start`) + +### 5. Получение URL +После успешного деплоя РІС‹ получите публичный URL для доступа Рє вашему приложению. + +## Особенности настройки + +### railway.json +Файл `railway.json` РІ РєРѕСЂРЅРµ проекта содержит базовую конфигурацию для Railway: +- Использует Nixpacks builder +- Команда запуска: `npm start` +- Политика перезапуска РїСЂРё ошибках + +### nixpacks.toml +Файл `nixpacks.toml` настраивает процесс СЃР±РѕСЂРєРё: +- Использует Node.js 20 +- Команда установки: `npm install` +- Команда СЃР±РѕСЂРєРё: `npm run build` + +### Dockerfile (альтернативный вариант) +Если возникают проблемы СЃ Nixpacks, Railway может автоматически использовать Dockerfile: +- Базируется РЅР° Node.js 20 Alpine +- Оптимизирован для продакшена +- Устанавливает только production зависимости + +### Пакетный менеджер +Проект использует npm (РЅРµ pnpm), поэтому убедитесь что: +- Есть файл `package-lock.json` +- Нет файла `pnpm-lock.yaml` РІ репозитории + +### Переменные окружения +Railway автоматически подставляет переменные окружения РёР· .env.local РІ продакшен. + +### Логи +Логи доступны РІ интерфейсе Railway РІ разделе "Logs". + +## Решение проблем + +### Ошибки Nixpacks +Если возникают ошибки СЃ Nixpacks: +1. **Проблема СЃ lightningcss** - добавлены python3 Рё gcc РІ nixpacks.toml +2. **Ошибки СЃР±РѕСЂРєРё** - проверьте логи РІ Railway Dashboard +3. **Альтернатива** - удалите `nixpacks.toml`, Railway использует Dockerfile +4. **lightningcss ошибки** - используйте node:20-slim вместо Alpine РІ Dockerfile + +### Ошибки СЃР±РѕСЂРєРё +- **lightningcss module errors** - используйте стандартный Linux образ (РЅРµ Alpine) +- Проверьте, что РІСЃРµ зависимости указаны РІ package.json +- Убедитесь, что TypeScript РєРѕРґ компилируется без ошибок +- Используйте `npm run build` локально для тестирования + +### Ошибки API ключей +- Проверьте правильность всех API ключей +- Убедитесь, что Сѓ ключей есть необходимые права доступа + +### Проблемы СЃ портом +Railway автоматически назначает РїРѕСЂС‚ через переменную PORT. Next.js это поддерживает РїРѕ умолчанию. + +### Переключение между Nixpacks Рё Docker +Railway автоматически выберет лучший метод СЃР±РѕСЂРєРё: +- Если `nixpacks.toml` работает - использует Nixpacks +- Если Nixpacks РЅРµ работает - переключается РЅР° Dockerfile +- РњРѕР¶РЅРѕ принудительно использовать Docker, удалив `nixpacks.toml` +- РџСЂРё проблемах СЃ lightningcss - рекомендуется использовать Dockerfile + +## Обновления +Railway автоматически пересобирает Рё разворачивает приложение РїСЂРё каждом push РІ главную ветку GitHub репозитория. diff --git a/README.md b/README.md index 803cc92be..f09fce485 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,76 @@ # Open Lovable -Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev ❤️](https://lovable.dev/). +Общайтесь с ИИ для мгновенного создания React приложений. Пример приложения от команды [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github). Для полного облачного решения, посетите [Lovable.dev ❤️](https://lovable.dev/). Open Lovable Demo +## Настройка - -## Setup - -1. **Clone & Install** +1. **Клонировать и установить** ```bash git clone https://github.com/mendableai/open-lovable.git cd open-lovable npm install ``` -2. **Add `.env.local`** +2. **Добавить `.env.local`** ```env -# Required -E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes) -FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) - -# Optional (need at least one AI provider) -ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com -OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) -GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey -GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) +# Обязательные +E2B_API_KEY=your_e2b_api_key # Получить с https://e2b.dev (Песочницы) +FIRECRAWL_API_KEY=your_firecrawl_api_key # Получить с https://firecrawl.dev (Парсинг веб-сайтов) + +# Дополнительные (нужен минимум один ИИ провайдер) +ANTHROPIC_API_KEY=your_anthropic_api_key # Получить с https://console.anthropic.com +OPENAI_API_KEY=your_openai_api_key # Получить с https://platform.openai.com (GPT-5) +GEMINI_API_KEY=your_gemini_api_key # Получить с https://aistudio.google.com/app/apikey +GROQ_API_KEY=your_groq_api_key # Получить с https://console.groq.com (Быстрый вывод - рекомендуется Kimi K2) + +# Supabase (опционально) +NEXT_PUBLIC_SUPABASE_URL=your_supabase_url # URL проекта Supabase +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key # Публичный ключ +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # Ключ для серверных операций ``` -3. **Run** +3. **Запустить** ```bash npm run dev ``` -Open [http://localhost:3000](http://localhost:3000) +Открыть [http://localhost:3000](http://localhost:3000) + +## Деплой на Railway + +1. **Создать аккаунт на [Railway](https://railway.app/)** + +2. **Подключить ваш GitHub репозиторий** + - Нажать "New Project" + - Выбрать "Deploy from GitHub repo" + - Выбрать ваш форк этого репозитория + +3. **Настроить переменные окружения** + В разделе Variables добавить: + ``` + E2B_API_KEY=your_e2b_api_key + FIRECRAWL_API_KEY=your_firecrawl_api_key + ANTHROPIC_API_KEY=your_anthropic_api_key + OPENAI_API_KEY=your_openai_api_key + GEMINI_API_KEY=your_gemini_api_key + GROQ_API_KEY=your_groq_api_key + NODE_ENV=production + ``` + +4. **Деплой автоматически начнётся** + Railway автоматически обнаружит Next.js приложение и развернёт его. + +5. **Получить URL** + После успешного деплоя вы получите публичный URL для вашего приложения. + +### Файлы для Railway + +- `railway.json` - конфигурация Railway +- `.env.example` - пример переменных окружения +- `package.json` - уже содержит необходимые скрипты для Railway -## License +## Лицензия -MIT +MIT \ No newline at end of file diff --git a/SUPABASE_INTEGRATION_SUMMARY.md b/SUPABASE_INTEGRATION_SUMMARY.md new file mode 100644 index 000000000..32c2e90f3 --- /dev/null +++ b/SUPABASE_INTEGRATION_SUMMARY.md @@ -0,0 +1,185 @@ +# рџЋ‰ Supabase Integration Complete! + +## вњ… Что было сделано + +### 1. Установка Рё конфигурация +- вњ… Установлен пакет `@supabase/supabase-js` (v2.58.0) +- вњ… Создан РѕСЃРЅРѕРІРЅРѕР№ клиент Supabase (`lib/supabase.ts`) +- вњ… Добавлены переменные окружения РІ `.env.example` + +### 2. Утилиты Рё хелперы +Создан полный набор helper-функций РІ `lib/supabase-helpers.ts`: +- **Authentication**: signUp, signIn, signOut, getCurrentUser, getSession +- **Database**: fetchFromTable, insertIntoTable, updateInTable, deleteFromTable +- **Storage**: uploadFile, getPublicUrl, downloadFile, deleteFile +- **Realtime**: subscribeToTable, unsubscribe +- **Server-side**: executeServerOperation + +### 3. React Hooks +Созданы готовые С…СѓРєРё РІ `lib/hooks/useSupabase.ts`: +- `useSupabaseAuth()` - управление аутентификацией +- `useSupabaseQuery()` - получение данных СЃ автоматическим обновлением +- `useSupabaseSubscription()` - realtime РїРѕРґРїРёСЃРєРё +- `useSupabaseMutation()` - операции вставки/обновления/удаления + +### 4. Примеры Рё документация +- вњ… `SUPABASE_SETUP.md` - полная документация СЃ примерами +- вњ… `SUPABASE_QUICKSTART.md` - быстрый старт +- вњ… `docs/SUPABASE_CHECKLIST.md` - чеклист для интеграции +- вњ… `components/SupabaseExample.tsx` - пример компонента +- вњ… `app/api/supabase-example/route.ts` - пример API endpoint +- вњ… `types/supabase.ts` - типы для TypeScript +- вњ… `scripts/test-supabase.js` - тестовый СЃРєСЂРёРїС‚ подключения +- вњ… `lib/supabase-projects.ts` - функции для работы СЃ проектами + +### 5. База данных +- вњ… `supabase/migrations/001_initial_schema.sql` - начальная схема БД +- вњ… Созданы таблицы: profiles, projects, project_files, generation_history +- вњ… Настроены RLS политики для всех таблиц +- вњ… Добавлены индексы для оптимизации +- вњ… Созданы триггеры для автоматизации + +### 6. Обновлена документация +- вњ… Обновлен `README.md` СЃ информацией Рѕ Supabase +- вњ… Обновлен `.env.example` СЃ переменными Supabase +- вњ… Добавлен npm СЃРєСЂРёРїС‚ `test:supabase` для тестирования + +## рџ”‘ Ваши учетные данные + +```env +NEXT_PUBLIC_SUPABASE_URL=https://lyuxhqhusukvpvwtkkum.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5dXhocWh1c3VrdnB2d3Rra3VtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTkxNzM0MjAsImV4cCI6MjA3NDc0OTQyMH0.JidO0voYsPldgFiaUYwAp-HmtOGLZnldW5Gyn0CMsYo +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5dXhocWh1c3VrdnB2d3Rra3VtIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTE3MzQyMCwiZXhwIjoyMDc0NzQ5NDIwfQ.LkxzPWN_T0jMmwJGwdkYs1Pkw01cYzf_4g4oSdxQcaE +``` + +## рџљЂ Следующие шаги + +### 1. Добавьте переменные окружения +Создайте файл `.env.local` Рё добавьте туда учетные данные выше. + +### 2. Перезапустите сервер +```bash +npm run dev +``` + +### 3. Создайте таблицы РІ Supabase +Перейдите РІ [SQL Editor](https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/editor) Рё выполните миграцию: + +```bash +# Скопируйте содержимое файла supabase/migrations/001_initial_schema.sql +# Вставьте РІ SQL Editor Рё нажмите Run +``` + +Или создайте СЃРІРѕРё таблицы: +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email TEXT UNIQUE NOT NULL, + name TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +ALTER TABLE users ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Users can view own data" ON users + FOR SELECT USING (auth.uid() = id); +``` + +### 4. Протестируйте подключение + +**Через тестовый СЃРєСЂРёРїС‚:** +```bash +npm run test:supabase +``` + +**Через API endpoint:** +Откройте РІ браузере: +``` +http://localhost:3000/api/supabase-example +``` + +### 5. Используйте РІ РєРѕРґРµ + +**Клиентский компонент:** +```typescript +import { useSupabaseAuth, useSupabaseQuery } from '@/lib/hooks/useSupabase'; + +export default function MyComponent() { + const { user } = useSupabaseAuth(); + const { data, loading } = useSupabaseQuery('users'); + + return
{/* ваш код */}
; +} +``` + +**API Route:** +```typescript +import { executeServerOperation } from '@/lib/supabase-helpers'; + +export async function GET() { + const data = await executeServerOperation(async (client) => { + const { data } = await client.from('users').select('*'); + return data; + }); + + return Response.json(data); +} +``` + +## 📁 Структура файлов + +``` +web-agency/ +├── lib/ +│ ├── supabase.ts # Основной клиент +│ ├── supabase-helpers.ts # Helper функции +│ ├── supabase-projects.ts # Функции для работы с проектами +│ └── hooks/ +│ └── useSupabase.ts # React hooks +├── app/ +│ └── api/ +│ └── supabase-example/ +│ └── route.ts # Пример API endpoint +├── components/ +│ └── SupabaseExample.tsx # Пример компонента +├── types/ +│ └── supabase.ts # TypeScript типы +├── scripts/ +│ └── test-supabase.js # Тестовый скрипт +├── supabase/ +│ ├── migrations/ +│ │ └── 001_initial_schema.sql # Начальная схема БД +│ └── README.md # Документация миграций +├── docs/ +│ └── SUPABASE_CHECKLIST.md # Чеклист +├── SUPABASE_SETUP.md # Полная документация +├── SUPABASE_QUICKSTART.md # Быстрый старт +└── .env.example # Обновлен с Supabase переменными +``` + +## 📚 Документация + +- **Быстрый старт**: `SUPABASE_QUICKSTART.md` +- **Полная документация**: `SUPABASE_SETUP.md` +- **Чеклист**: `docs/SUPABASE_CHECKLIST.md` + +## 🔗 Полезные ссылки + +- **Ваш проект**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum +- **SQL Editor**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/editor +- **Документация Supabase**: https://supabase.com/docs +- **Supabase JS Client**: https://supabase.com/docs/reference/javascript/introduction + +## 💡 Советы + +1. **Безопасность**: Service Role Key используйте ТОЛЬКО на сервере (API routes) +2. **RLS**: Всегда настраивайте Row Level Security для защиты данных +3. **Типы**: Генерируйте TypeScript типы из схемы для type-safety +4. **Realtime**: Включайте Realtime только для нужных таблиц +5. **Индексы**: Создавайте индексы для часто запрашиваемых полей + +## 🎯 Готово к использованию! + +Интеграция Supabase полностью настроена и готова к использованию. Следуйте документации и примерам для начала работы. + +Удачи! 🚀 diff --git a/SUPABASE_QUICKSTART.md b/SUPABASE_QUICKSTART.md new file mode 100644 index 000000000..b6b90dd33 --- /dev/null +++ b/SUPABASE_QUICKSTART.md @@ -0,0 +1,90 @@ +# 🚀 Supabase Quick Start + +## Шаг 1: Добавьте переменные окружения + +Создайте или обновите файл `.env.local`: + +```env +NEXT_PUBLIC_SUPABASE_URL=https://lyuxhqhusukvpvwtkkum.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5dXhocWh1c3VrdnB2d3Rra3VtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTkxNzM0MjAsImV4cCI6MjA3NDc0OTQyMH0.JidO0voYsPldgFiaUYwAp-HmtOGLZnldW5Gyn0CMsYo +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5dXhocWh1c3VrdnB2d3Rra3VtIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTE3MzQyMCwiZXhwIjoyMDc0NzQ5NDIwfQ.LkxzPWN_T0jMmwJGwdkYs1Pkw01cYzf_4g4oSdxQcaE +``` + +## Шаг 2: Перезапустите сервер + +```bash +npm run dev +``` + +## Шаг 3: Используйте Supabase + +### В клиентских компонентах: + +```typescript +import { useSupabaseAuth, useSupabaseQuery } from '@/lib/hooks/useSupabase'; + +export default function MyComponent() { + const { user } = useSupabaseAuth(); + const { data, loading } = useSupabaseQuery('my_table'); + + return
{/* ваш код */}
; +} +``` + +### В API routes: + +```typescript +import { executeServerOperation } from '@/lib/supabase-helpers'; + +export async function GET() { + const data = await executeServerOperation(async (client) => { + const { data } = await client.from('my_table').select('*'); + return data; + }); + + return Response.json(data); +} +``` + +## Шаг 4: Создайте таблицы в Supabase + +Перейдите в [Supabase Dashboard](https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum) и создайте таблицы через SQL Editor: + +```sql +-- Пример таблицы пользователей +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email TEXT UNIQUE NOT NULL, + name TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Включить RLS +ALTER TABLE users ENABLE ROW LEVEL SECURITY; + +-- Политика доступа +CREATE POLICY "Users can view own data" ON users + FOR SELECT USING (auth.uid() = id); +``` + +## 📚 Полная документация + +См. `SUPABASE_SETUP.md` для подробных примеров и всех доступных функций. + +## 🧪 Тестирование подключения + +Откройте в браузере: +``` +http://localhost:3000/api/supabase-example +``` + +Или используйте пример компонента: +```typescript +import SupabaseExample from '@/components/SupabaseExample'; +``` + +## 🔗 Полезные ссылки + +- **Ваш проект**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum +- **Документация**: https://supabase.com/docs +- **SQL Editor**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/editor diff --git a/SUPABASE_SETUP.md b/SUPABASE_SETUP.md new file mode 100644 index 000000000..20e1bd33f --- /dev/null +++ b/SUPABASE_SETUP.md @@ -0,0 +1,211 @@ +# Supabase Integration Setup + +## 📋 Конфигурация + +Ваш проект настроен для работы с Supabase. Добавьте следующие переменные окружения в `.env.local`: + +```env +# Supabase Configuration +NEXT_PUBLIC_SUPABASE_URL=https://lyuxhqhusukvpvwtkkum.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5dXhocWh1c3VrdnB2d3Rra3VtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTkxNzM0MjAsImV4cCI6MjA3NDc0OTQyMH0.JidO0voYsPldgFiaUYwAp-HmtOGLZnldW5Gyn0CMsYo +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx5dXhocWh1c3VrdnB2d3Rra3VtIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTE3MzQyMCwiZXhwIjoyMDc0NzQ5NDIwfQ.LkxzPWN_T0jMmwJGwdkYs1Pkw01cYzf_4g4oSdxQcaE +``` + +## 🚀 Установка + +Пакет `@supabase/supabase-js` уже установлен. Если нужно переустановить: + +```bash +npm install @supabase/supabase-js +``` + +## 📁 Файлы + +### `lib/supabase.ts` +Основной клиент Supabase с конфигурацией для client-side и server-side операций. + +### `lib/supabase-helpers.ts` +Набор готовых helper-функций для работы с Supabase: +- **Authentication**: sign up, sign in, sign out, get user +- **Database**: CRUD операции +- **Storage**: upload, download, delete файлов +- **Realtime**: подписка на изменения в таблицах + +## 💡 Примеры использования + +### 1. Аутентификация + +```typescript +import { signUp, signIn, signOut, getCurrentUser } from '@/lib/supabase-helpers'; + +// Регистрация +const user = await signUp('user@example.com', 'password123', { + full_name: 'John Doe' +}); + +// Вход +const session = await signIn('user@example.com', 'password123'); + +// Получить текущего пользователя +const currentUser = await getCurrentUser(); + +// Выход +await signOut(); +``` + +### 2. Работа с базой данных + +```typescript +import { + fetchFromTable, + insertIntoTable, + updateInTable, + deleteFromTable +} from '@/lib/supabase-helpers'; + +// Получить данные +const users = await fetchFromTable('users', { + select: 'id, name, email', + filters: { status: 'active' }, + orderBy: { column: 'created_at', ascending: false }, + limit: 10 +}); + +// Вставить данные +const newUser = await insertIntoTable('users', { + name: 'John Doe', + email: 'john@example.com', + status: 'active' +}); + +// Обновить данные +await updateInTable('users', + { id: 1 }, + { name: 'Jane Doe' } +); + +// Удалить данные +await deleteFromTable('users', { id: 1 }); +``` + +### 3. Работа с файлами (Storage) + +```typescript +import { + uploadFile, + getPublicUrl, + downloadFile, + deleteFile +} from '@/lib/supabase-helpers'; + +// Загрузить файл +const file = document.querySelector('input[type="file"]').files[0]; +await uploadFile('avatars', `user-${userId}.png`, file); + +// Получить публичный URL +const url = getPublicUrl('avatars', `user-${userId}.png`); + +// Скачать файл +const blob = await downloadFile('avatars', `user-${userId}.png`); + +// Удалить файл +await deleteFile('avatars', `user-${userId}.png`); +``` + +### 4. Realtime подписки + +```typescript +import { subscribeToTable, unsubscribe } from '@/lib/supabase-helpers'; + +// Подписаться на изменения +const channel = subscribeToTable('messages', (payload) => { + console.log('Change received!', payload); +}, { + event: 'INSERT', // или 'UPDATE', 'DELETE', '*' + filter: 'room_id=eq.1' +}); + +// Отписаться +await unsubscribe(channel); +``` + +### 5. Server-side операции (API Routes) + +```typescript +import { executeServerOperation } from '@/lib/supabase-helpers'; + +// В API route +export async function POST(request: Request) { + const result = await executeServerOperation(async (serverClient) => { + // Используем serverClient с service_role правами + const { data, error } = await serverClient + .from('admin_data') + .select('*'); + + return data; + }); + + return Response.json(result); +} +``` + +### 6. Прямое использование клиента + +```typescript +import { supabase } from '@/lib/supabase'; + +// Для более сложных запросов +const { data, error } = await supabase + .from('posts') + .select(` + id, + title, + author:users(name, email), + comments(count) + `) + .eq('published', true) + .gte('created_at', '2024-01-01') + .order('created_at', { ascending: false }); +``` + +## 🔐 Безопасность + +- **NEXT_PUBLIC_SUPABASE_ANON_KEY** - безопасно использовать на клиенте (публичный ключ) +- **SUPABASE_SERVICE_ROLE_KEY** - использовать ТОЛЬКО на сервере (API routes) +- Настройте Row Level Security (RLS) в Supabase для защиты данных + +## 📚 Дополнительные ресурсы + +- [Supabase Documentation](https://supabase.com/docs) +- [Supabase JS Client](https://supabase.com/docs/reference/javascript/introduction) +- [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security) + +## 🎯 Следующие шаги + +1. Создайте таблицы в Supabase Dashboard +2. Настройте Row Level Security (RLS) политики +3. Создайте Storage buckets если нужно работать с файлами +4. Интегрируйте аутентификацию в ваше приложение +5. Используйте helper-функции для работы с данными + +## 🔄 Миграции + +Для создания таблиц можно использовать SQL Editor в Supabase Dashboard или создать миграции: + +```sql +-- Пример создания таблицы users +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email TEXT UNIQUE NOT NULL, + name TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Включить RLS +ALTER TABLE users ENABLE ROW LEVEL SECURITY; + +-- Создать политику (пользователи видят только свои данные) +CREATE POLICY "Users can view own data" ON users + FOR SELECT USING (auth.uid() = id); +``` diff --git a/USAGE_LIMITS_SETUP.md b/USAGE_LIMITS_SETUP.md new file mode 100644 index 000000000..d7ccf6856 --- /dev/null +++ b/USAGE_LIMITS_SETUP.md @@ -0,0 +1,273 @@ +# 📊 Система ограничений по тарифам + +## ✅ Что было создано + +### 1. База данных +- **`supabase/migrations/002_add_usage_limits.sql`** - миграция с таблицами и функциями +- Таблицы: + - `usage_history` - история использования запросов + - `subscription_history` - история подписок +- Функции: + - `get_remaining_requests()` - получить оставшиеся запросы + - `increment_usage()` - увеличить счетчик использования + - `set_subscription_tier()` - установить тариф + - `reset_monthly_usage()` - сбросить месячное использование + - `expire_subscriptions()` - истечь просроченные подписки + +### 2. TypeScript библиотека +- **`lib/supabase-usage.ts`** - функции для работы с лимитами +- **`lib/hooks/useUsageLimits.ts`** - React hook для отображения лимитов + +### 3. Компоненты +- **`components/UsageLimitBadge.tsx`** - бейдж с оставшимися запросами + +### 4. API Endpoints +- **`/api/check-usage`** - проверка лимитов (GET) и использование запроса (POST) +- **`/api/set-subscription`** - установка подписки (для тестирования) + +## 💰 Тарифные планы + +| Тариф | Запросов | Стоимость | +|-------|----------|-----------| +| **Бесплатный** | 0 | Бесплатно | +| **Базовый** | 5 | По вашей цене | +| **Профессиональный** | 15 | По вашей цене | + +## 🚀 Установка + +### Шаг 1: Примените миграцию + +Откройте [Supabase SQL Editor](https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/editor) и выполните содержимое файла: +``` +supabase/migrations/002_add_usage_limits.sql +``` + +### Шаг 2: Добавьте бейдж с лимитами + +В нужное место добавьте компонент: + +```tsx +import UsageLimitBadge from '@/components/UsageLimitBadge'; + +// В JSX: + +``` + +**Рекомендуемые места:** +- В хедере рядом с кнопкой пользователя +- В личном кабинете +- Перед кнопкой отправки запроса + +### Шаг 3: Проверяйте лимиты перед запросами + +В вашем API endpoint для генерации кода: + +```typescript +import { checkUserLimit, incrementUsage } from '@/lib/supabase-usage'; + +export async function POST(request: NextRequest) { + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Проверить лимит + const usage = await checkUserLimit(user.id); + + if (usage.requests_remaining <= 0) { + return NextResponse.json({ + error: 'Request limit reached', + ...usage + }, { status: 429 }); + } + + // Выполнить запрос к AI + const result = await generateCode(prompt); + + // Увеличить счетчик + await incrementUsage(user.id, 'ai_generation', result.tokens); + + return NextResponse.json({ success: true, ...result }); +} +``` + +## 📱 Использование в компонентах + +### Отображение лимитов + +```tsx +import { useUsageLimits } from '@/lib/hooks/useUsageLimits'; + +function MyComponent() { + const { usage, loading, hasRequestsRemaining, isLimitReached } = useUsageLimits(); + + if (loading) return
Загрузка...
; + + if (isLimitReached) { + return ( +
+

Лимит запросов исчерпан!

+

Обновите подписку для продолжения.

+
+ ); + } + + return ( +
+

Осталось запросов: {usage?.requests_remaining}

+ +
+ ); +} +``` + +### Проверка перед отправкой + +```tsx +import { getUserUsage } from '@/lib/supabase-usage'; + +async function handleSubmit() { + const usage = await getUserUsage(); + + if (!usage || usage.requests_remaining <= 0) { + alert('Лимит запросов исчерпан!'); + return; + } + + // Отправить запрос через API + const response = await fetch('/api/check-usage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ requestType: 'ai_generation' }) + }); + + const result = await response.json(); + + if (!result.success) { + alert(result.error); + return; + } + + // Продолжить с генерацией + await generateCode(); +} +``` + +## 🧪 Тестирование + +### Установить подписку для тестирования + +```bash +# Базовый тариф (5 запросов) +curl -X POST http://localhost:3000/api/set-subscription \ + -H "Content-Type: application/json" \ + -d '{"tier": "basic", "durationDays": 30}' + +# Профессиональный тариф (15 запросов) +curl -X POST http://localhost:3000/api/set-subscription \ + -H "Content-Type: application/json" \ + -d '{"tier": "professional", "durationDays": 30}' +``` + +### Проверить текущее использование + +```bash +curl http://localhost:3000/api/check-usage +``` + +### Использовать запрос + +```bash +curl -X POST http://localhost:3000/api/check-usage \ + -H "Content-Type": "application/json" \ + -d '{"requestType": "ai_generation", "tokens": 1000}' +``` + +## 🔄 Автоматические задачи + +### Сброс месячного использования + +Создайте cron job или используйте Supabase Edge Functions: + +```sql +-- Вызывать раз в месяц +SELECT public.reset_monthly_usage(); +``` + +### Истечение подписок + +```sql +-- Вызывать ежедневно +SELECT public.expire_subscriptions(); +``` + +## 💳 Интеграция с платежами + +После успешной оплаты в вашем payment webhook: + +```typescript +import { setSubscriptionTier } from '@/lib/supabase-usage'; + +// В webhook обработчике +const userId = payment.metadata.user_id; +const tier = payment.metadata.tier; // 'basic' или 'professional' + +await setSubscriptionTier(userId, tier, 30); +``` + +## 📊 Отображение в личном кабинете + +Добавьте в `app/dashboard/page.tsx`: + +```tsx +import { useUsageLimits } from '@/lib/hooks/useUsageLimits'; +import { getTierName } from '@/lib/supabase-usage'; + +function Dashboard() { + const { usage } = useUsageLimits(); + + return ( +
+
+
Тариф
+
{getTierName(usage?.subscription_tier || 'free')}
+
+
+
Использовано
+
{usage?.requests_used} / {usage?.requests_limit}
+
+
+
Осталось
+
{usage?.requests_remaining}
+
+
+ ); +} +``` + +## рџЋЁ Стилизация бейджа + +Бейдж автоматически меняет цвет: +- рџџў **Зеленый** - РјРЅРѕРіРѕ запросов осталось +- рџџЎ **Желтый** - осталось 2 или меньше +- 🔴 **Красный** - лимит исчерпан + +## вљ пёЏ Важные замечания + +1. **Безопасность**: Всегда проверяйте лимиты РЅР° сервере, РЅРµ только РЅР° клиенте +2. **Производительность**: Используйте кеширование для частых проверок +3. **UX**: Показывайте пользователю сколько запросов осталось ДО отправки +4. **Уведомления**: Предупреждайте РєРѕРіРґР° остается мало запросов + +## рџ”— Полезные ссылки + +- **Supabase Dashboard**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum +- **SQL Editor**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/editor +- **Table Editor**: https://supabase.com/dashboard/project/lyuxhqhusukvpvwtkkum/editor + +--- + +**Готово!** Система ограничений РїРѕ тарифам полностью настроена! рџЋ‰ diff --git a/agent.md b/agent.md new file mode 100644 index 000000000..5bc7d411e --- /dev/null +++ b/agent.md @@ -0,0 +1,52 @@ +# Agent Architecture + +## Purpose +This document captures how the Open Lovable project orchestrates AI-assisted code generation and sandbox management. + +## High-Level Flow +1. The Next.js UI (`app/page.tsx`) collects user prompts and keeps local session state (sandbox data, chat log, generation progress). +2. When a prompt is submitted, the client calls `/api/generate-ai-code-stream` with the active sandbox context. +3. The streaming endpoint enriches the prompt with file/context metadata, optionally calls `/api/analyze-edit-intent` to build a search plan, executes that plan against the cached file manifest, then streams model output back to the browser. +4. Model responses are parsed on the server (`/api/apply-ai-code`) to detect ``, ``, and `` directives. Files are written into the remote E2B sandbox and package installs/commands are triggered via dedicated APIs. +5. Sandbox progress and Vite status are polled through monitoring endpoints; the UI reflects changes through `CodeApplicationProgress` and live iframe previews. + +## Generation Modes +- **Reference URL (default)**: The home overlay accepts a source URL and a target industry. The client scrapes the reference site, captures an optional screenshot, and instructs the model to rebuild the layout for the new niche while reusing the original visual rhythm. +- **Prompt Builder**: Users can switch modes to provide a free-form brief. The shared `runGenerationRequest` utility streams generation without scraping, building a fresh React/Tailwind experience from the supplied narrative, optional industry, and extra instructions. + +## Frontend Orchestrator +- **App Router + Client Page**: `app/page.tsx` manages the AI workflow, keeps conversation context, and drives UI state (tabs, sandbox tree, preview iframe, generation file list). +- **Theme & UI primitives**: Theme toggling lives under `app/components/*`, while reusable inputs/buttons reside in `components/ui/*` (shadcn/tailwind style). +- **Progress & Preview**: `components/CodeApplicationProgress.tsx` renders streaming status, and `components/SandboxPreview.tsx` handles iframe previewing of the sandbox build. + +## Serverless/Agent Endpoints (app/api) +- `create-ai-sandbox`: spins up an E2B sandbox, seeds a Vite + Tailwind starter, stores handles in `global.activeSandbox` and `global.sandboxState`. +- `generate-ai-code-stream`: main orchestrator; selects an AI client (`@ai-sdk/*` wrappers), prunes conversation state, builds enhanced prompts via `lib/context-selector`, runs search plans through `lib/file-search-executor`, and streams `text/event-stream` chunks. +- `analyze-edit-intent`: validates the user request against the manifest and returns a structured search plan (terms, regex, expected matches) using the `ai` SDK's `generateObject` helper. +- `apply-ai-code`: parses XML/HTML-like tags in model output, deduplicates files, installs packages (delegating to `install-packages`), writes files into the sandbox, and records results for the UI. +- Support endpoints (`apply-ai-code-stream`, `detect-and-install-packages`, `monitor-vite-logs`, `sandbox-status`, etc.) expose operational controls so the UI can react without direct sandbox access. + +## Context Building & Search Heuristics +- `lib/context-selector.ts` combines manifest metadata and heuristics (e.g., always include `App.jsx`, Tailwind config) to prime the model with precise instructions and constraints. +- `lib/edit-intent-analyzer.ts` classifies user requests into `EditType`s and predicts target files using regex-driven resolvers that traverse the manifest's component tree. +- `lib/file-search-executor.ts` runs AI-generated search plans across cached file contents to produce high-confidence line-level hits that get injected into the system prompt before generation. + +## State & Data Contracts +- **SandboxState** (`types/sandbox.ts`): cached manifest, file contents, and metadata for the current E2B sandbox. +- **ConversationState** (`types/conversation.ts`): rolling history (messages, edits, user preferences) kept in `global.conversationState` to personalize future prompts. +- **FileManifest** (`types/file-manifest.ts`): normalized view of project files, imports, component relationships, and routes; created when the sandbox is initialized and reused by search/intents. + +## External Dependencies +- **E2B Code Interpreter**: remote execution environment for applying code, installing packages, and running Vite. +- **AI Providers**: Anthropic, OpenAI, Groq (for OSS GPT), Google Gemini via `@ai-sdk/*` clients; selected per request based on `config/app.config.ts`. +- **Framer Motion/Tailwind**: used on the client for rich animations and theming. + +## Configuration & Deployment +- `config/app.config.ts` centralizes knobs (model list, sandbox timeouts, retry policies, UI flags). +- Environment variables (`.env.example`, `.env.local`) carry API keys for E2B, Firecrawl, and each LLM provider. +- Multiple deployment targets are supported: Dockerfiles (`Dockerfile`, `.simple`, `.extended`), `nixpacks.toml`, `railway.json`, and docs (`RAILWAY_DEPLOY.md`) outline hosting on Railway. + +## Notable Operational Considerations +- Several server files rely on Node global state; deploy to a single process or ensure globals are scoped per worker. +- Some legacy text assets still contain mojibake (e.g., `.env.example`, `README.md`, `layout.tsx` metadata); convert to UTF-8 before production. +- Test scripts (`npm run test:*`) reference `tests/*.js`, but that folder is absent locally; recreate or adjust before enabling CI gating. diff --git a/app/api/analyze-edit-intent/route.ts b/app/api/analyze-edit-intent/route.ts index 7cf35bc1a..f8c2d68ab 100644 --- a/app/api/analyze-edit-intent/route.ts +++ b/app/api/analyze-edit-intent/route.ts @@ -104,7 +104,10 @@ export async function POST(request: NextRequest) { aiModel = openai(model.replace('openai/', '')); } } else if (model.startsWith('google/')) { - aiModel = createGoogleGenerativeAI(model.replace('google/', '')); + const google = createGoogleGenerativeAI({ + apiKey: process.env.GOOGLE_API_KEY, + }); + aiModel = google(model.replace('google/', '')); } else { // Default to groq if model format is unclear aiModel = groq(model); diff --git a/app/api/apply-ai-code-stream/route.ts b/app/api/apply-ai-code-stream/route.ts index c91bf113c..fa37a99d1 100644 --- a/app/api/apply-ai-code-stream/route.ts +++ b/app/api/apply-ai-code-stream/route.ts @@ -119,7 +119,7 @@ function parseAIResponse(response: string): ParsedResponse { for (const pkg of filePackages) { if (!sections.packages.includes(pkg)) { sections.packages.push(pkg); - console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`); + console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`); } } } @@ -139,7 +139,7 @@ function parseAIResponse(response: string): ParsedResponse { for (const pkg of filePackages) { if (!sections.packages.includes(pkg)) { sections.packages.push(pkg); - console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`); + console.log(`[apply-ai-code-stream] 📦 Package detected from imports: ${pkg}`); } } } diff --git a/app/api/check-usage/route.ts b/app/api/check-usage/route.ts new file mode 100644 index 000000000..f49d0de6d --- /dev/null +++ b/app/api/check-usage/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { checkUserLimit, incrementUsage } from '@/lib/supabase-usage'; +import { createServerSupabaseClient } from '@/lib/supabase'; + +// Force dynamic rendering +export const dynamic = 'force-dynamic'; + +/** + * GET /api/check-usage - Check user's remaining requests + */ +export async function GET(request: NextRequest) { + try { + const client = createServerSupabaseClient(); + const { data: { user }, error: authError } = await client.auth.getUser(); + + if (authError || !user) { + return NextResponse.json({ + success: false, + error: 'Unauthorized' + }, { status: 401 }); + } + + const usage = await checkUserLimit(user.id); + + return NextResponse.json({ + success: true, + ...usage + }); + } catch (error: any) { + console.error('Error checking usage:', error); + return NextResponse.json({ + success: false, + error: error.message || 'Failed to check usage' + }, { status: 500 }); + } +} + +/** + * POST /api/check-usage - Increment usage counter + * Body: { requestType?: string, tokens?: number } + */ +export async function POST(request: NextRequest) { + try { + const client = createServerSupabaseClient(); + const { data: { user }, error: authError } = await client.auth.getUser(); + + if (authError || !user) { + return NextResponse.json({ + success: false, + error: 'Unauthorized' + }, { status: 401 }); + } + + const body = await request.json(); + const { requestType = 'ai_generation', tokens = 0 } = body; + + // Check if user has requests remaining + const currentUsage = await checkUserLimit(user.id); + + if (currentUsage.requests_remaining <= 0) { + return NextResponse.json({ + success: false, + error: 'Request limit reached', + ...currentUsage + }, { status: 429 }); // Too Many Requests + } + + // Increment usage + const result = await incrementUsage(user.id, requestType, tokens); + + return NextResponse.json(result); + } catch (error: any) { + console.error('Error incrementing usage:', error); + return NextResponse.json({ + success: false, + error: error.message || 'Failed to increment usage' + }, { status: 500 }); + } +} diff --git a/app/api/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts index 257ce1db5..f37398da5 100644 --- a/app/api/create-ai-sandbox/route.ts +++ b/app/api/create-ai-sandbox/route.ts @@ -86,7 +86,7 @@ package_json = { with open('/home/user/app/package.json', 'w') as f: json.dump(package_json, f, indent=2) -print('✓ package.json') +print('вњ“ package.json') # Vite config for E2B - with allowedHosts vite_config = """import { defineConfig } from 'vite' @@ -106,7 +106,7 @@ export default defineConfig({ with open('/home/user/app/vite.config.js', 'w') as f: f.write(vite_config) -print('✓ vite.config.js') +print('вњ“ vite.config.js') # Tailwind config - standard without custom design tokens tailwind_config = """/** @type {import('tailwindcss').Config} */ @@ -123,7 +123,7 @@ export default { with open('/home/user/app/tailwind.config.js', 'w') as f: f.write(tailwind_config) -print('✓ tailwind.config.js') +print('вњ“ tailwind.config.js') # PostCSS config postcss_config = """export default { @@ -135,7 +135,7 @@ postcss_config = """export default { with open('/home/user/app/postcss.config.js', 'w') as f: f.write(postcss_config) -print('✓ postcss.config.js') +print('вњ“ postcss.config.js') # Index.html index_html = """ @@ -153,7 +153,7 @@ index_html = """ with open('/home/user/app/index.html', 'w') as f: f.write(index_html) -print('✓ index.html') +print('вњ“ index.html') # Main.jsx main_jsx = """import React from 'react' @@ -169,7 +169,7 @@ ReactDOM.createRoot(document.getElementById('root')).render( with open('/home/user/app/src/main.jsx', 'w') as f: f.write(main_jsx) -print('✓ src/main.jsx') +print('вњ“ src/main.jsx') # App.jsx with explicit Tailwind test app_jsx = """function App() { @@ -189,7 +189,7 @@ export default App""" with open('/home/user/app/src/App.jsx', 'w') as f: f.write(app_jsx) -print('✓ src/App.jsx') +print('вњ“ src/App.jsx') # Index.css with explicit Tailwind directives index_css = """@tailwind base; @@ -220,7 +220,7 @@ body { with open('/home/user/app/src/index.css', 'w') as f: f.write(index_css) -print('✓ src/index.css') +print('вњ“ src/index.css') print('\\nAll files created successfully!') `; @@ -243,9 +243,9 @@ result = subprocess.run( ) if result.returncode == 0: - print('✓ Dependencies installed successfully') + print('вњ“ Dependencies installed successfully') else: - print(f'⚠ Warning: npm install had issues: {result.stderr}') + print(f'вљ  Warning: npm install had issues: {result.stderr}') # Continue anyway as it might still work `); @@ -273,7 +273,7 @@ process = subprocess.Popen( env=env ) -print(f'✓ Vite dev server started with PID: {process.pid}') +print(f'вњ“ Vite dev server started with PID: {process.pid}') print('Waiting for server to be ready...') `); @@ -289,11 +289,11 @@ import time css_file = '/home/user/app/src/index.css' if os.path.exists(css_file): os.utime(css_file, None) - print('✓ Triggered CSS rebuild') + print('вњ“ Triggered CSS rebuild') # Also ensure PostCSS processes it time.sleep(2) -print('✓ Tailwind CSS should be loaded') +print('вњ“ Tailwind CSS should be loaded') `); // Store sandbox globally diff --git a/app/api/detect-and-install-packages/route.ts b/app/api/detect-and-install-packages/route.ts index 12211b651..99f3d527d 100644 --- a/app/api/detect-and-install-packages/route.ts +++ b/app/api/detect-and-install-packages/route.ts @@ -184,7 +184,7 @@ for package in packages_to_install: if os.path.exists(package_path): installed.append(package) - print(f"✓ Verified installation of {package}") + print(f"вњ“ Verified installation of {package}") else: # Check if it's a submodule of an installed package base_package = package.split('/')[0] @@ -195,10 +195,10 @@ for package in packages_to_install: base_path = f"/home/user/app/node_modules/{base_package}" if os.path.exists(base_path): installed.append(package) - print(f"✓ Verified installation of {package} (via {base_package})") + print(f"вњ“ Verified installation of {package} (via {base_package})") else: failed.append(package) - print(f"✗ Failed to verify installation of {package}") + print(f"вњ— Failed to verify installation of {package}") result_data = { 'installed': installed, diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index eaae15d73..52e55f8c5 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -168,9 +168,14 @@ export async function POST(request: NextRequest) { const manifest: FileManifest | undefined = global.sandboxState?.fileCache?.manifest; if (manifest) { - await sendProgress({ type: 'status', message: '🔍 Creating search plan...' }); + await sendProgress({ type: 'status', message: 'рџ”Ќ Creating search plan...' }); - const fileContents = global.sandboxState.fileCache.files; + const fileCache = global.sandboxState?.fileCache; + if (!fileCache) { + throw new Error('File cache is not available'); + } + + const fileContents = fileCache.files; console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length); // STEP 1: Get search plan from AI @@ -187,7 +192,7 @@ export async function POST(request: NextRequest) { await sendProgress({ type: 'status', - message: `🔎 Searching for: "${searchPlan.searchTerms.join('", "')}"` + message: `рџ”Ћ Searching for: "${searchPlan.searchTerms.join('", "')}"` }); // STEP 2: Execute the search plan @@ -214,7 +219,7 @@ export async function POST(request: NextRequest) { if (target) { await sendProgress({ type: 'status', - message: `✅ Found code in ${target.filePath.split('/').pop()} at line ${target.lineNumber}` + message: `вњ… Found code in ${target.filePath.split('/').pop()} at line ${target.lineNumber}` }); console.log('[generate-ai-code-stream] Target selected:', target); @@ -257,7 +262,7 @@ User request: "${prompt}"`; console.warn('[generate-ai-code-stream] Search found no results, falling back to broader context'); await sendProgress({ type: 'status', - message: '⚠️ Could not find exact match, using broader search...' + message: 'вљ пёЏ Could not find exact match, using broader search...' }); } } else { @@ -267,7 +272,7 @@ User request: "${prompt}"`; console.error('[generate-ai-code-stream] Error in agentic search workflow:', error); await sendProgress({ type: 'status', - message: '⚠️ Search workflow error, falling back to keyword method...' + message: 'вљ пёЏ Search workflow error, falling back to keyword method...' }); // Fall back to old method on any error if we have a manifest if (manifest) { @@ -283,7 +288,7 @@ User request: "${prompt}"`; console.log('[generate-ai-code-stream] No manifest available for fallback'); await sendProgress({ type: 'status', - message: '⚠️ No file manifest available, will use broad context' + message: 'вљ пёЏ No file manifest available, will use broad context' }); } } @@ -331,7 +336,7 @@ User request: "${prompt}"`; // For now, fall back to keyword search since we don't have file contents for search execution // This path happens when no manifest was initially available - let targetFiles = []; + let targetFiles: string[] = []; if (!searchPlan || searchPlan.searchTerms.length === 0) { console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files'); @@ -494,7 +499,7 @@ Remember: You are a SURGEON making a precise incision, not an artist repainting console.log('[generate-ai-code-stream] Including', recentEdits.length, 'recent edits in context'); conversationContext += `\n### Recent Edits:\n`; recentEdits.forEach(edit => { - conversationContext += `- "${edit.userRequest}" → ${edit.editType} (${edit.targetFiles.map(f => f.split('/').pop()).join(', ')})\n`; + conversationContext += `- "${edit.userRequest}" в†’ ${edit.editType} (${edit.targetFiles.map(f => f.split('/').pop()).join(', ')})\n`; }); } @@ -509,7 +514,7 @@ Remember: You are a SURGEON making a precise incision, not an artist repainting if (recentlyCreatedFiles.length > 0) { const uniqueFiles = [...new Set(recentlyCreatedFiles)]; - conversationContext += `\n### 🚨 RECENTLY CREATED/EDITED FILES (DO NOT RECREATE THESE):\n`; + conversationContext += `\n### рџљЁ RECENTLY CREATED/EDITED FILES (DO NOT RECREATE THESE):\n`; uniqueFiles.forEach(file => { conversationContext += `- ${file}\n`; }); @@ -554,7 +559,7 @@ Remember: You are a SURGEON making a precise incision, not an artist repainting const systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications. ${conversationContext} -🚨 CRITICAL RULES - YOUR MOST IMPORTANT INSTRUCTIONS: +рџљЁ CRITICAL RULES - YOUR MOST IMPORTANT INSTRUCTIONS: 1. **DO EXACTLY WHAT IS ASKED - NOTHING MORE, NOTHING LESS** - Don't add features not requested - Don't fix unrelated issues @@ -562,14 +567,21 @@ ${conversationContext} 2. **CHECK App.jsx FIRST** - ALWAYS see what components exist before creating new ones 3. **NAVIGATION LIVES IN Header.jsx** - Don't create Nav.jsx if Header exists with nav 4. **USE STANDARD TAILWIND CLASSES ONLY**: - - ✅ CORRECT: bg-white, text-black, bg-blue-500, bg-gray-100, text-gray-900 - - ❌ WRONG: bg-background, text-foreground, bg-primary, bg-muted, text-secondary + - вњ… CORRECT: bg-white, text-black, bg-blue-500, bg-gray-100, text-gray-900 + - вќЊ WRONG: bg-background, text-foreground, bg-primary, bg-muted, text-secondary - Use ONLY classes from the official Tailwind CSS documentation 5. **FILE COUNT LIMITS**: - Simple style/text change = 1 file ONLY - New component = 2 files MAX (component + parent) - If >3 files, YOU'RE DOING TOO MUCH +SPECIAL INSTRUCTION FOR TILDA BLOCKS: +If the user mentions "Tilda block" or "Tilda", you should focus on creating a single, self-contained section that looks like a Tilda block. +- Use clean, modern design. +- Focus on the specific block type requested (e.g., "cover block", "features block", "text block"). +- Although Tilda uses its own class system, you should simulate it using standard Tailwind CSS to ensure it renders correctly in this sandbox. +- Ensure the block is responsive. + COMPONENT RELATIONSHIPS (CHECK THESE FIRST): - Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx - Logo is typically in Header, not standalone @@ -641,7 +653,7 @@ TARGETED EDIT MODE ACTIVE - Confidence: ${editContext.editIntent.confidence} - Files to Edit: ${editContext.primaryFiles.join(', ')} -🚨 CRITICAL RULE - VIOLATION WILL RESULT IN FAILURE 🚨 +рџљЁ CRITICAL RULE - VIOLATION WILL RESULT IN FAILURE рџљЁ YOU MUST ***ONLY*** GENERATE THE FILES LISTED ABOVE! ABSOLUTE REQUIREMENTS: @@ -655,18 +667,18 @@ ABSOLUTE REQUIREMENTS: 8. DO NOT add bonus features EXAMPLE VIOLATIONS (THESE ARE FAILURES): -❌ User says "update the hero" → You update Hero, Header, Footer, and App.jsx -❌ User says "change header color" → You redesign the entire header -❌ User says "fix the button" → You update multiple components -❌ Files to Edit shows "Hero.jsx" → You also generate App.jsx "to integrate it" -❌ Files to Edit shows "Header.jsx" → You also update Footer.jsx "for consistency" +вќЊ User says "update the hero" в†’ You update Hero, Header, Footer, and App.jsx +вќЊ User says "change header color" в†’ You redesign the entire header +вќЊ User says "fix the button" в†’ You update multiple components +вќЊ Files to Edit shows "Hero.jsx" в†’ You also generate App.jsx "to integrate it" +вќЊ Files to Edit shows "Header.jsx" в†’ You also update Footer.jsx "for consistency" CORRECT BEHAVIOR (THIS IS SUCCESS): -✅ User says "update the hero" → You ONLY edit Hero.jsx with the requested change -✅ User says "change header color" → You ONLY change the color in Header.jsx -✅ User says "fix the button" → You ONLY fix the specific button issue -✅ Files to Edit shows "Hero.jsx" → You generate ONLY Hero.jsx -✅ Files to Edit shows "Header.jsx, Nav.jsx" → You generate EXACTLY 2 files: Header.jsx and Nav.jsx +вњ… User says "update the hero" в†’ You ONLY edit Hero.jsx with the requested change +вњ… User says "change header color" в†’ You ONLY change the color in Header.jsx +вњ… User says "fix the button" в†’ You ONLY fix the specific button issue +вњ… Files to Edit shows "Hero.jsx" в†’ You generate ONLY Hero.jsx +вњ… Files to Edit shows "Header.jsx, Nav.jsx" в†’ You generate EXACTLY 2 files: Header.jsx and Nav.jsx THE AI INTENT ANALYZER HAS ALREADY DETERMINED THE FILES. DO NOT SECOND-GUESS IT. @@ -758,9 +770,9 @@ CRITICAL STRING AND SYNTAX RULES: - ALWAYS escape quotes properly in JSX attributes - NEVER use curly quotes or smart quotes ('' "" '' "") - only straight quotes (' ") - ALWAYS convert smart/curly quotes to straight quotes: - - ' and ' → ' - - " and " → " - - Any other Unicode quotes → straight quotes + - ' and ' в†’ ' + - " and " в†’ " + - Any other Unicode quotes в†’ straight quotes - When strings contain apostrophes, either: 1. Use double quotes: "you're" instead of 'you're' 2. Escape the apostrophe: 'you\'re' @@ -808,9 +820,9 @@ WHEN WORKING WITH SCRAPED CONTENT: - ALWAYS sanitize all text content before using in code - Convert ALL smart quotes to straight quotes - Example transformations: - - "Firecrawl's API" → "Firecrawl's API" or "Firecrawl\\'s API" - - 'It's amazing' → "It's amazing" or 'It\\'s amazing' - - "Best tool ever" → "Best tool ever" + - "Firecrawl's API" в†’ "Firecrawl's API" or "Firecrawl\\'s API" + - 'It's amazing' в†’ "It's amazing" or 'It\\'s amazing' + - "Best tool ever" в†’ "Best tool ever" - When in doubt, use double quotes for strings containing apostrophes - For testimonials or quotes from scraped content, ALWAYS clean the text: - Bad: content: 'Moved our internal agent's web scraping...' @@ -855,13 +867,13 @@ CRITICAL COMPLETION RULES: With 16,000 tokens available, you have plenty of space to generate a complete application. Use it! UNDERSTANDING USER INTENT FOR INCREMENTAL VS FULL GENERATION: -- "add/create/make a [specific feature]" → Add ONLY that feature to existing app -- "add a videos page" → Create ONLY Videos.jsx and update routing -- "update the header" → Modify ONLY header component -- "fix the styling" → Update ONLY the affected components -- "change X to Y" → Find the file containing X and modify it -- "make the header black" → Find Header component and change its color -- "rebuild/recreate/start over" → Full regeneration +- "add/create/make a [specific feature]" в†’ Add ONLY that feature to existing app +- "add a videos page" в†’ Create ONLY Videos.jsx and update routing +- "update the header" в†’ Modify ONLY header component +- "fix the styling" в†’ Update ONLY the affected components +- "change X to Y" в†’ Find the file containing X and modify it +- "make the header black" в†’ Find Header component and change its color +- "rebuild/recreate/start over" в†’ Full regeneration - Default to incremental updates when working on an existing app SURGICAL EDIT RULES (CRITICAL FOR PERFORMANCE): @@ -877,11 +889,11 @@ SURGICAL EDIT RULES (CRITICAL FOR PERFORMANCE): - If you're editing >3 files for a simple request, STOP - you're doing too much EXAMPLES OF CORRECT SURGICAL EDITS: -✅ "change header to black" → Find className="..." in Header.jsx, change ONLY color classes -✅ "update hero text" → Find the

or

in Hero.jsx, change ONLY the text inside -✅ "add a button to hero" → Find the return statement, ADD button, keep everything else -❌ WRONG: Regenerating entire Header.jsx to change one color -❌ WRONG: Rewriting Hero.jsx to add one button +вњ… "change header to black" в†’ Find className="..." in Header.jsx, change ONLY color classes +вњ… "update hero text" в†’ Find the

or

in Hero.jsx, change ONLY the text inside +вњ… "add a button to hero" в†’ Find the return statement, ADD button, keep everything else +вќЊ WRONG: Regenerating entire Header.jsx to change one color +вќЊ WRONG: Rewriting Hero.jsx to add one button NAVIGATION/HEADER INTELLIGENCE: - ALWAYS check App.jsx imports first @@ -953,16 +965,19 @@ CRITICAL: When files are provided in the context: } // Store files in cache - for (const [path, content] of Object.entries(filesData.files)) { - const normalizedPath = path.replace('/home/user/app/', ''); - global.sandboxState.fileCache.files[normalizedPath] = { - content: content as string, - lastModified: Date.now() - }; - } - - if (filesData.manifest) { - global.sandboxState.fileCache.manifest = filesData.manifest; + const fileCacheForUpdate = global.sandboxState?.fileCache; + if (fileCacheForUpdate) { + for (const [path, content] of Object.entries(filesData.files)) { + const normalizedPath = path.replace('/home/user/app/', ''); + fileCacheForUpdate.files[normalizedPath] = { + content: content as string, + lastModified: Date.now() + }; + } + + if (filesData.manifest) { + fileCacheForUpdate.manifest = filesData.manifest; + } // Now try to analyze edit intent with the fetched manifest if (!editContext) { @@ -993,9 +1008,12 @@ CRITICAL: When files are provided in the context: } // Update variables - backendFiles = global.sandboxState.fileCache.files; - hasBackendFiles = Object.keys(backendFiles).length > 0; - console.log('[generate-ai-code-stream] Updated backend cache with fetched files'); + const fileCacheForRead = global.sandboxState?.fileCache; + if (fileCacheForRead) { + backendFiles = fileCacheForRead.files; + hasBackendFiles = Object.keys(backendFiles).length > 0; + console.log('[generate-ai-code-stream] Updated backend cache with fetched files'); + } } } } catch (error) { @@ -1044,7 +1062,7 @@ CRITICAL: When files are provided in the context: } } - contextParts.push('\n🚨 CRITICAL INSTRUCTIONS - VIOLATION = FAILURE 🚨'); + contextParts.push('\nрџљЁ CRITICAL INSTRUCTIONS - VIOLATION = FAILURE рџљЁ'); contextParts.push('1. Analyze the user request: "' + prompt + '"'); contextParts.push('2. Identify the MINIMUM number of files that need editing (usually just ONE)'); contextParts.push('3. PRESERVE ALL EXISTING CONTENT in those files'); @@ -1052,19 +1070,19 @@ CRITICAL: When files are provided in the context: contextParts.push('5. DO NOT regenerate entire components from scratch'); contextParts.push('6. DO NOT change unrelated parts of any file'); contextParts.push('7. Generate ONLY the files that MUST be changed - NO EXTRAS'); - contextParts.push('\n⚠️ FILE COUNT RULE:'); + contextParts.push('\nвљ пёЏ FILE COUNT RULE:'); contextParts.push('- Simple change (color, text, spacing) = 1 file ONLY'); contextParts.push('- Adding new component = 2 files MAX (new component + parent that imports it)'); contextParts.push('- DO NOT exceed these limits unless absolutely necessary'); contextParts.push('\nEXAMPLES OF CORRECT BEHAVIOR:'); - contextParts.push('✅ "add a chart to the hero" → Edit ONLY Hero.jsx, ADD the chart, KEEP everything else'); - contextParts.push('✅ "change header to black" → Edit ONLY Header.jsx, change ONLY the color'); - contextParts.push('✅ "fix spacing in footer" → Edit ONLY Footer.jsx, adjust ONLY spacing'); + contextParts.push('вњ… "add a chart to the hero" в†’ Edit ONLY Hero.jsx, ADD the chart, KEEP everything else'); + contextParts.push('вњ… "change header to black" в†’ Edit ONLY Header.jsx, change ONLY the color'); + contextParts.push('вњ… "fix spacing in footer" в†’ Edit ONLY Footer.jsx, adjust ONLY spacing'); contextParts.push('\nEXAMPLES OF FAILURES:'); - contextParts.push('❌ "change header color" → You edit Header, Footer, and App "for consistency"'); - contextParts.push('❌ "add chart to hero" → You regenerate the entire Hero component'); - contextParts.push('❌ "fix button" → You update 5 different component files'); - contextParts.push('\n⚠️ FINAL WARNING:'); + contextParts.push('вќЊ "change header color" в†’ You edit Header, Footer, and App "for consistency"'); + contextParts.push('вќЊ "add chart to hero" в†’ You regenerate the entire Hero component'); + contextParts.push('вќЊ "fix button" в†’ You update 5 different component files'); + contextParts.push('\nвљ пёЏ FINAL WARNING:'); contextParts.push('If you generate MORE files than necessary, you have FAILED'); contextParts.push('If you DELETE or REWRITE existing functionality, you have FAILED'); contextParts.push('ONLY change what was EXPLICITLY requested - NOTHING MORE'); @@ -1090,7 +1108,7 @@ CRITICAL: When files are provided in the context: contextParts.push('This is an incremental update to an existing application.'); contextParts.push('DO NOT regenerate App.jsx, index.css, or other core files unless explicitly requested.'); contextParts.push('ONLY create or modify the specific files needed for the user\'s request.'); - contextParts.push('\n⚠️ CRITICAL FILE OUTPUT FORMAT - VIOLATION = FAILURE:'); + contextParts.push('\nвљ пёЏ CRITICAL FILE OUTPUT FORMAT - VIOLATION = FAILURE:'); contextParts.push('YOU MUST OUTPUT EVERY FILE IN THIS EXACT XML FORMAT:'); contextParts.push(''); contextParts.push('// Complete file content here'); @@ -1098,13 +1116,13 @@ CRITICAL: When files are provided in the context: contextParts.push(''); contextParts.push('/* CSS content here */'); contextParts.push(''); - contextParts.push('\n❌ NEVER OUTPUT: "Generated Files: index.css, App.jsx"'); - contextParts.push('❌ NEVER LIST FILE NAMES WITHOUT CONTENT'); - contextParts.push('✅ ALWAYS: One tag per file with COMPLETE content'); - contextParts.push('✅ ALWAYS: Include EVERY file you modified'); + contextParts.push('\nвќЊ NEVER OUTPUT: "Generated Files: index.css, App.jsx"'); + contextParts.push('вќЊ NEVER LIST FILE NAMES WITHOUT CONTENT'); + contextParts.push('вњ… ALWAYS: One tag per file with COMPLETE content'); + contextParts.push('вњ… ALWAYS: Include EVERY file you modified'); } else if (!hasBackendFiles) { // First generation mode - make it beautiful! - contextParts.push('\n🎨 FIRST GENERATION MODE - CREATE SOMETHING BEAUTIFUL!'); + contextParts.push('\nрџЋЁ FIRST GENERATION MODE - CREATE SOMETHING BEAUTIFUL!'); contextParts.push('\nThis is the user\'s FIRST experience. Make it impressive:'); contextParts.push('1. **USE TAILWIND PROPERLY** - Use standard Tailwind color classes'); contextParts.push('2. **NO PLACEHOLDERS** - Use real content, not lorem ipsum'); @@ -1112,7 +1130,7 @@ CRITICAL: When files are provided in the context: contextParts.push('4. **VISUAL POLISH** - Shadows, hover states, transitions'); contextParts.push('5. **STANDARD CLASSES** - bg-white, text-gray-900, bg-blue-500, NOT bg-background'); contextParts.push('\nCreate a polished, professional application that works perfectly on first load.'); - contextParts.push('\n⚠️ OUTPUT FORMAT:'); + contextParts.push('\nвљ пёЏ OUTPUT FORMAT:'); contextParts.push('Use content tags for EVERY file'); contextParts.push('NEVER output "Generated Files:" as plain text'); } @@ -1155,7 +1173,18 @@ CRITICAL: When files are provided in the context: const isAnthropic = model.startsWith('anthropic/'); const isGoogle = model.startsWith('google/'); const isOpenAI = model.startsWith('openai/gpt-5'); - const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq)); + + let modelProvider; + if (isAnthropic) { + modelProvider = anthropic; + } else if (isOpenAI) { + modelProvider = openai; + } else if (isGoogle) { + modelProvider = googleGenerativeAI; + } else { + modelProvider = groq; + } + const actualModel = isAnthropic ? model.replace('anthropic/', '') : (model === 'openai/gpt-5') ? 'gpt-5' : (isGoogle ? model.replace('google/', '') : model); @@ -1168,7 +1197,7 @@ CRITICAL: When files are provided in the context: role: 'system', content: systemPrompt + ` -🚨 CRITICAL CODE GENERATION RULES - VIOLATION = FAILURE 🚨: +рџљЁ CRITICAL CODE GENERATION RULES - VIOLATION = FAILURE рџљЁ: 1. NEVER truncate ANY code - ALWAYS write COMPLETE files 2. NEVER use "..." anywhere in your code - this causes syntax errors 3. NEVER cut off strings mid-sentence - COMPLETE every string @@ -1189,16 +1218,16 @@ PACKAGE RULES: - NEVER install packages like @mendable/firecrawl-js unless explicitly requested Examples of SYNTAX ERRORS (NEVER DO THIS): -❌ className="px-4 py-2 bg-blue-600 hover:bg-blue-7... -❌ + + + + + {/* Main content */} +

+ {/* Stats */} +
+
+
+
+

Всего проектов

+

+ {projects.length} +

+
+
+ +
+
+
+ +
+
+
+

Активных

+

+ {projects.filter(p => p.status === 'active').length} +

+
+
+ +
+
+
+ +
+
+
+

В архиве

+

+ {projects.filter(p => p.status === 'archived').length} +

+
+
+ +
+
+
+
+ + {/* Projects list */} +
+
+

+ Мои проекты +

+
+ + {loading ? ( +
+
+

Загрузка проектов...

+
+ ) : error ? ( +
+

{error}

+
+ ) : projects.length === 0 ? ( +
+ +

+ Нет проектов +

+

+ Создайте свой первый проект с помощью AI +

+ +
+ ) : ( +
+ {projects.map((project) => ( +
+
+
+

+ {project.name} +

+ {project.description && ( +

+ {project.description} +

+ )} +
+ + Создан: {new Date(project.created_at).toLocaleDateString('ru-RU')} + +
+
+
+ +
+
+
+ ))} +
+ )} +
+
+ + ); +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index e12cb7d7f..7bac5784a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,11 @@ @import "tailwindcss"; + +.font-display { + font-family: var(--font-display), var(--font-sans, system-ui); + letter-spacing: -0.01em; +} + @keyframes slide { 0% { transform: translate(0, 0); } 100% { transform: translate(70px, 70px); } diff --git a/app/layout.tsx b/app/layout.tsx index 7cd0cd301..b98ca00f5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,13 +1,15 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Inter, Space_Grotesk } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/app/components/theme-provider"; +import { AuthProvider } from "@/contexts/AuthContext"; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ["latin", "cyrillic"], variable: "--font-sans" }); +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"], variable: "--font-display" }); export const metadata: Metadata = { - title: "Open Lovable", - description: "Re-imagine any website in seconds with AI-powered website builder.", + title: "REBUILDR — AI Web Agency", + description: "REBUILDR создаёт выразительные цифровые лендинги и прототипы, превращая сырые идеи в продукт с ясным смыслом.", }; export default function RootLayout({ @@ -16,17 +18,20 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + + + + - {children} + {children} ); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 2eedb3716..6da49f4d2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,23 @@ -'use client'; +'use client'; + + + +import { useState, useEffect, useRef, Suspense } from 'react'; -import { useState, useEffect, useRef } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; + import { appConfig } from '@/config/app.config'; + import { Button } from '@/components/ui/button'; + import { Textarea } from '@/components/ui/textarea'; + import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; + import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; + // Import icons from centralized module to avoid Turbopack chunk issues + import { FiFile, FiChevronRight, @@ -16,3411 +26,7691 @@ import { BsFolderFill, BsFolder2Open, SiJavascript, - SiReact, + SiReact, SiCss3, SiJson } from '@/lib/icons'; + import { motion, AnimatePresence } from 'framer-motion'; + import CodeApplicationProgress, { type CodeApplicationState } from '@/components/CodeApplicationProgress'; + import { ThemeToggle } from '@/app/components/theme-toggle'; + import { ThemeLogo } from '@/app/components/theme-logo'; +import UserButton from '@/components/auth/UserButton'; + +import HomeScreenHeader from '@/components/HomeScreenHeader'; + + + interface SandboxData { + sandboxId: string; + url: string; + [key: string]: any; + } + + interface ChatMessage { + content: string; + type: 'user' | 'ai' | 'system' | 'file-update' | 'command' | 'error'; + timestamp: Date; + metadata?: { + scrapedUrl?: string; + scrapedContent?: any; + generatedCode?: string; + appliedFiles?: string[]; + commandType?: 'input' | 'output' | 'error' | 'success'; + }; + } -export default function AISandboxPage() { + + +function AISandboxPageContent() { + const [sandboxData, setSandboxData] = useState(null); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState({ text: 'Not connected', active: false }); + const [responseArea, setResponseArea] = useState([]); + const [structureContent, setStructureContent] = useState('No sandbox created yet'); + const [promptInput, setPromptInput] = useState(''); + const [chatMessages, setChatMessages] = useState([ + { - content: 'Welcome! I can help you generate code with full context of your sandbox files and structure. Just start chatting - I\'ll automatically create a sandbox for you if needed!\n\nTip: If you see package errors like "react-router-dom not found", just type "npm install" or "check packages" to automatically install missing packages.', + + content: 'Добро пожаловать! Я могу помочь вам сгенерировать код с полным контекстом файлов и структуры вашего проекта. Просто начните общаться — я автоматически создам песочницу для вас, если это потребуется.\n\nСовет: Если вы видите ошибки пакетов типа "react-router-dom не найден", просто напишите "npm install" или "проверить пакеты" для автоматической установки недостающих зависимостей.', + type: 'system', + timestamp: new Date() + } + ]); + const [aiChatInput, setAiChatInput] = useState(''); + const [aiEnabled] = useState(true); + const searchParams = useSearchParams(); + const router = useRouter(); + const [aiModel, setAiModel] = useState(() => { + const modelParam = searchParams.get('model'); + return appConfig.ai.availableModels.includes(modelParam || '') ? modelParam! : appConfig.ai.defaultModel; + }); + const [urlOverlayVisible, setUrlOverlayVisible] = useState(false); + const [urlInput, setUrlInput] = useState(''); + const [urlStatus, setUrlStatus] = useState([]); + const [showHomeScreen, setShowHomeScreen] = useState(true); + const [expandedFolders, setExpandedFolders] = useState>(new Set(['app', 'src', 'src/components'])); + const [selectedFile, setSelectedFile] = useState(null); + const [homeScreenFading, setHomeScreenFading] = useState(false); + const [homeUrlInput, setHomeUrlInput] = useState(''); + const [homeContextInput, setHomeContextInput] = useState(''); + + const [homePromptInput, setHomePromptInput] = useState(''); + + const [homeMode, setHomeMode] = useState<'url' | 'prompt'>('url'); + + const [homeIndustryInput, setHomeIndustryInput] = useState(''); + const [activeTab, setActiveTab] = useState<'generation' | 'preview'>('preview'); + const [showStyleSelector, setShowStyleSelector] = useState(false); + const [selectedStyle, setSelectedStyle] = useState(null); + const [showLoadingBackground, setShowLoadingBackground] = useState(false); + const [urlScreenshot, setUrlScreenshot] = useState(null); + const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false); + const [screenshotError, setScreenshotError] = useState(null); + const [isPreparingDesign, setIsPreparingDesign] = useState(false); + const [targetUrl, setTargetUrl] = useState(''); + const [loadingStage, setLoadingStage] = useState<'gathering' | 'planning' | 'generating' | null>(null); + const [sandboxFiles, setSandboxFiles] = useState>({}); + const [fileStructure, setFileStructure] = useState(''); + + const [conversationContext, setConversationContext] = useState<{ + scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>; + generatedComponents: Array<{ name: string; path: string; content: string }>; + appliedCode: Array<{ files: string[]; timestamp: Date }>; + currentProject: string; + lastGeneratedCode?: string; + + targetIndustry?: string; + }>({ + scrapedWebsites: [], + generatedComponents: [], + appliedCode: [], + currentProject: '', + + targetIndustry: '', + lastGeneratedCode: undefined + }); + + const iframeRef = useRef(null); + const chatMessagesRef = useRef(null); + const codeDisplayRef = useRef(null); + + const [codeApplicationState, setCodeApplicationState] = useState({ + stage: null + }); + + const [generationProgress, setGenerationProgress] = useState<{ + isGenerating: boolean; + status: string; + components: Array<{ name: string; path: string; completed: boolean }>; + currentComponent: number; + streamedCode: string; + isStreaming: boolean; + isThinking: boolean; + thinkingText?: string; + thinkingDuration?: number; + currentFile?: { path: string; content: string; type: string }; + files: Array<{ path: string; content: string; type: string; completed: boolean }>; + lastProcessedPosition: number; + isEdit?: boolean; + }>({ + isGenerating: false, + status: '', + components: [], + currentComponent: 0, + streamedCode: '', + isStreaming: false, + isThinking: false, + files: [], + lastProcessedPosition: 0 + }); + + + const handlePayment = async (plan: 'basic' | 'professional') => { + + try { + + const response = await fetch('/api/payment', { + + method: 'POST', + + headers: { 'Content-Type': 'application/json' }, + + body: JSON.stringify({ plan }) + + }); + + + + const data = await response.json(); + + + + if (data.success) { + + window.open(data.paymentUrl, '_blank'); + + } + + } catch (error) { + + console.error('Payment error:', error); + + } + + }; + + + // Clear old conversation data on component mount and create/restore sandbox + useEffect(() => { + let isMounted = true; + + const initializePage = async () => { + // Clear old conversation + try { + await fetch('/api/conversation-state', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'clear-old' }) + }); + console.log('[home] Cleared old conversation data on mount'); + } catch (error) { + console.error('[ai-sandbox] Failed to clear old conversation:', error); + if (isMounted) { + addChatMessage('Failed to clear old conversation data.', 'error'); + } + } + + if (!isMounted) return; + + // Check if sandbox ID is in URL + const sandboxIdParam = searchParams.get('sandbox'); + + + if (!sandboxIdParam) { + + setLoading(false); + + console.log('[home] No sandbox ID present, waiting for user action.'); + + return; + + } + setLoading(true); + try { - if (sandboxIdParam) { - console.log('[home] Attempting to restore sandbox:', sandboxIdParam); - // For now, just create a new sandbox - you could enhance this to actually restore - // the specific sandbox if your backend supports it - await createSandbox(true); - } else { - console.log('[home] No sandbox in URL, creating new sandbox automatically...'); - await createSandbox(true); - } + + console.log('[home] Attempting to restore sandbox:', sandboxIdParam); + + // For now, just create a new sandbox - you could enhance this to restore a specific sandbox if supported + + await createSandbox(true); + } catch (error) { + console.error('[ai-sandbox] Failed to create or restore sandbox:', error); + if (isMounted) { + addChatMessage('Failed to create or restore sandbox.', 'error'); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + initializePage(); + + return () => { + isMounted = false; + }; + }, []); // Run only on mount + + useEffect(() => { + // Handle Escape key for home screen + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && showHomeScreen) { + setHomeScreenFading(true); + setTimeout(() => { + setShowHomeScreen(false); + setHomeScreenFading(false); + }, 500); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [showHomeScreen]); + + // Start capturing screenshot if URL is provided on mount (from home screen) + useEffect(() => { + if (!showHomeScreen && homeUrlInput && !urlScreenshot && !isCapturingScreenshot) { + let screenshotUrl = homeUrlInput.trim(); + if (!screenshotUrl.match(/^https?:\/\//i)) { + screenshotUrl = 'https://' + screenshotUrl; + } + captureUrlScreenshot(screenshotUrl); + } + }, [showHomeScreen, homeUrlInput]); // eslint-disable-line react-hooks/exhaustive-deps + + + useEffect(() => { + // Only check sandbox status on mount and when user navigates to the page + checkSandboxStatus(); + + // Optional: Check status when window regains focus + const handleFocus = () => { + checkSandboxStatus(); + }; + + window.addEventListener('focus', handleFocus); + return () => window.removeEventListener('focus', handleFocus); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (chatMessagesRef.current) { + chatMessagesRef.current.scrollTop = chatMessagesRef.current.scrollHeight; + } + }, [chatMessages]); + + + const updateStatus = (text: string, active: boolean) => { + setStatus({ text, active }); + }; + + const log = (message: string, type: 'info' | 'error' | 'command' = 'info') => { + setResponseArea(prev => [...prev, `[${type}] ${message}`]); + }; + + const addChatMessage = (content: string, type: ChatMessage['type'], metadata?: ChatMessage['metadata']) => { + setChatMessages(prev => { + // Skip duplicate consecutive system messages + if (type === 'system' && prev.length > 0) { + const lastMessage = prev[prev.length - 1]; + if (lastMessage.type === 'system' && lastMessage.content === content) { + return prev; // Skip duplicate + } + } + return [...prev, { content, type, timestamp: new Date(), metadata }]; + }); + }; + + const checkAndInstallPackages = async () => { + if (!sandboxData) { + addChatMessage('No active sandbox. Create a sandbox first!', 'system'); + return; + } + + // Vite error checking removed - handled by template setup + addChatMessage('Sandbox is ready. Vite configuration is handled by the template.', 'system'); + }; + + const handleSurfaceError = (errors: any[]) => { + // Function kept for compatibility but Vite errors are now handled by template + + // Focus the input + const textarea = document.querySelector('textarea') as HTMLTextAreaElement; + if (textarea) { + textarea.focus(); + } + }; + + const installPackages = async (packages: string[]) => { + if (!sandboxData) { + addChatMessage('No active sandbox. Create a sandbox first!', 'system'); + return; + } + + try { + const response = await fetch('/api/install-packages', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ packages }) + }); + + if (!response.ok) { + throw new Error(`Failed to install packages: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + while (reader) { + 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: ')) { + try { + const data = JSON.parse(line.slice(6)); + + switch (data.type) { + case 'command': + // Don't show npm install commands - they're handled by info messages + if (!data.command.includes('npm install')) { + addChatMessage(data.command, 'command', { commandType: 'input' }); + } + break; + case 'output': + addChatMessage(data.message, 'command', { commandType: 'output' }); + break; + case 'error': + if (data.message && data.message !== 'undefined') { + addChatMessage(data.message, 'command', { commandType: 'error' }); + } + break; + case 'warning': + addChatMessage(data.message, 'command', { commandType: 'output' }); + break; + case 'success': + addChatMessage(`${data.message}`, 'system'); + break; + case 'status': + addChatMessage(data.message, 'system'); + break; + } + } catch (e) { + console.error('Failed to parse SSE data:', e); + } + } + } + } + } catch (error: any) { + addChatMessage(`Failed to install packages: ${error.message}`, 'system'); + } + }; + + const checkSandboxStatus = async () => { + try { + const response = await fetch('/api/sandbox-status'); + const data = await response.json(); + + if (data.active && data.healthy && data.sandboxData) { + setSandboxData(data.sandboxData); + updateStatus('Sandbox active', true); + } else if (data.active && !data.healthy) { + // Sandbox exists but not responding + updateStatus('Sandbox not responding', false); + // Optionally try to create a new one + } else { + setSandboxData(null); + updateStatus('No sandbox', false); + } + } catch (error) { + console.error('Failed to check sandbox status:', error); + setSandboxData(null); + updateStatus('Error', false); + } + }; + + const createSandbox = async (fromHomeScreen = false) => { + console.log('[createSandbox] Starting sandbox creation...'); + setLoading(true); + setShowLoadingBackground(true); - updateStatus('Creating sandbox...', false); + + updateStatus('Создание песочницы...', false); + setResponseArea([]); + setScreenshotError(null); + + try { + const response = await fetch('/api/create-ai-sandbox', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}) + }); + + const data = await response.json(); + console.log('[createSandbox] Response data:', data); + + if (data.success) { + setSandboxData(data); + updateStatus('Sandbox active', true); - log('Sandbox created successfully!'); + + log('Песочница создана успешно!'); + log(`Sandbox ID: ${data.sandboxId}`); + log(`URL: ${data.url}`); + + // Update URL with sandbox ID + const newParams = new URLSearchParams(searchParams.toString()); + newParams.set('sandbox', data.sandboxId); + newParams.set('model', aiModel); + router.push(`/?${newParams.toString()}`, { scroll: false }); + + // Fade out loading background after sandbox loads + setTimeout(() => { + setShowLoadingBackground(false); + }, 3000); + + if (data.structure) { + displayStructure(data.structure); + } + + // Fetch sandbox files after creation + setTimeout(fetchSandboxFiles, 1000); + + // Restart Vite server to ensure it's running + setTimeout(async () => { + try { + console.log('[createSandbox] Ensuring Vite server is running...'); + const restartResponse = await fetch('/api/restart-vite', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + if (restartResponse.ok) { + const restartData = await restartResponse.json(); + if (restartData.success) { + console.log('[createSandbox] Vite server started successfully'); + } + } + } catch (error) { + console.error('[createSandbox] Error starting Vite server:', error); + } + }, 2000); + + // Only add welcome message if not coming from home screen + if (!fromHomeScreen) { + addChatMessage(`Sandbox created! ID: ${data.sandboxId}. I now have context of your sandbox and can help you build your app. Just ask me to create components and I'll automatically apply them! + + Tip: I automatically detect and install npm packages from your code imports (like react-router-dom, axios, etc.)`, 'system'); + } + + setTimeout(() => { + if (iframeRef.current) { + iframeRef.current.src = data.url; + } + }, 100); + } else { + throw new Error(data.error || 'Unknown error'); + } + } catch (error: any) { + console.error('[createSandbox] Error:', error); + updateStatus('Error', false); + log(`Failed to create sandbox: ${error.message}`, 'error'); + addChatMessage(`Failed to create sandbox: ${error.message}`, 'system'); + } finally { + setLoading(false); + } + }; + + const displayStructure = (structure: any) => { + if (typeof structure === 'object') { + setStructureContent(JSON.stringify(structure, null, 2)); + } else { + setStructureContent(structure || 'No structure available'); + } + }; + + const applyGeneratedCode = async (code: string, isEdit: boolean = false) => { + setLoading(true); + log('Applying AI-generated code...'); + + try { + // Show progress component instead of individual messages + setCodeApplicationState({ stage: 'analyzing' }); + + // Get pending packages from tool calls + const pendingPackages = ((window as any).pendingPackages || []).filter((pkg: any) => pkg && typeof pkg === 'string'); + if (pendingPackages.length > 0) { + console.log('[applyGeneratedCode] Sending packages from tool calls:', pendingPackages); + // Clear pending packages after use + (window as any).pendingPackages = []; + } + + // Use streaming endpoint for real-time feedback + const response = await fetch('/api/apply-ai-code-stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + response: code, + isEdit: isEdit, + packages: pendingPackages, + sandboxId: sandboxData?.sandboxId // Pass the sandbox ID to ensure proper connection + }) + }); + + if (!response.ok) { + throw new Error(`Failed to apply code: ${response.statusText}`); + } + + // Handle streaming response + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + let finalData: any = null; + + while (reader) { + 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: ')) { + try { + const data = JSON.parse(line.slice(6)); + + switch (data.type) { + case 'start': + // Don't add as chat message, just update state + setCodeApplicationState({ stage: 'analyzing' }); + break; + + case 'step': + // Update progress state based on step + if (data.message.includes('Installing') && data.packages) { + setCodeApplicationState({ + stage: 'installing', + packages: data.packages + }); + } else if (data.message.includes('Creating files') || data.message.includes('Applying')) { + setCodeApplicationState({ + stage: 'applying', - filesGenerated: results.filesCreated + + filesGenerated: data.filesCreated || [] + }); + } + break; + + case 'package-progress': + // Handle package installation progress + if (data.installedPackages) { + setCodeApplicationState(prev => ({ + ...prev, + installedPackages: data.installedPackages + })); + } + break; + + case 'command': + // Don't show npm install commands - they're handled by info messages + if (data.command && !data.command.includes('npm install')) { + addChatMessage(data.command, 'command', { commandType: 'input' }); + } + break; + + case 'success': + if (data.installedPackages) { + setCodeApplicationState(prev => ({ + ...prev, + installedPackages: data.installedPackages + })); + } + break; + + case 'file-progress': + // Skip file progress messages, they're noisy + break; + + case 'file-complete': + // Could add individual file completion messages if desired + break; + + case 'command-progress': + addChatMessage(`${data.action} command: ${data.command}`, 'command', { commandType: 'input' }); + break; + + case 'command-output': + addChatMessage(data.output, 'command', { + commandType: data.stream === 'stderr' ? 'error' : 'output' + }); + break; + + case 'command-complete': + if (data.success) { + addChatMessage(`Command completed successfully`, 'system'); + } else { + addChatMessage(`Command failed with exit code ${data.exitCode}`, 'system'); + } + break; + + case 'complete': + finalData = data; + setCodeApplicationState({ stage: 'complete' }); + // Clear the state after a delay + setTimeout(() => { + setCodeApplicationState({ stage: null }); + }, 3000); + break; + + case 'error': + addChatMessage(`Error: ${data.message || data.error || 'Unknown error'}`, 'system'); + break; + + case 'warning': + addChatMessage(`${data.message}`, 'system'); + break; + + case 'info': + // Show info messages, especially for package installation + if (data.message) { + addChatMessage(data.message, 'system'); + } + break; + } + } catch (e) { + // Ignore parse errors + } + } + } + } + + // Process final data + if (finalData && finalData.type === 'complete') { + const data = { + success: true, + results: finalData.results, + explanation: finalData.explanation, + structure: finalData.structure, - message: finalData.message + + message: finalData.message, + + autoCompleted: finalData.autoCompleted, + + autoCompletedComponents: finalData.autoCompletedComponents, + + warning: finalData.warning, + + missingImports: finalData.missingImports, + + debug: finalData.debug + }; + + if (data.success) { + const { results } = data; + + // Log package installation results without duplicate messages + if (results.packagesInstalled?.length > 0) { + log(`Packages installed: ${results.packagesInstalled.join(', ')}`); + } + + if (results.filesCreated?.length > 0) { + log('Files created:'); + results.filesCreated.forEach((file: string) => { + log(` ${file}`, 'command'); + }); + + // Verify files were actually created by refreshing the sandbox if needed + if (sandboxData?.sandboxId && results.filesCreated.length > 0) { + // Small delay to ensure files are written + setTimeout(() => { + // Force refresh the iframe to show new files + if (iframeRef.current) { + iframeRef.current.src = iframeRef.current.src; + } + }, 1000); + } + } + + if (results.filesUpdated?.length > 0) { + log('Files updated:'); + results.filesUpdated.forEach((file: string) => { + log(` ${file}`, 'command'); + }); + } + + // Update conversation context with applied code + setConversationContext(prev => ({ + ...prev, + appliedCode: [...prev.appliedCode, { + files: [...(results.filesCreated || []), ...(results.filesUpdated || [])], + timestamp: new Date() + }] + })); + + if (results.commandsExecuted?.length > 0) { + log('Commands executed:'); + results.commandsExecuted.forEach((cmd: string) => { + log(` $ ${cmd}`, 'command'); + }); + } + + if (results.errors?.length > 0) { + results.errors.forEach((err: string) => { + log(err, 'error'); + }); + } + + if (data.structure) { + displayStructure(data.structure); + } + + if (data.explanation) { + log(data.explanation); + } + + if (data.autoCompleted) { + log('Auto-generating missing components...', 'command'); + + if (data.autoCompletedComponents) { + setTimeout(() => { + log('Auto-generated missing components:', 'info'); + data.autoCompletedComponents.forEach((comp: string) => { + log(` ${comp}`, 'command'); + }); + }, 1000); + } + } else if (data.warning) { + log(data.warning, 'error'); + + if (data.missingImports && data.missingImports.length > 0) { + const missingList = data.missingImports.join(', '); + addChatMessage( + `Ask me to "create the missing components: ${missingList}" to fix these import errors.`, + 'system' + ); + } + } + + log('Code applied successfully!'); + console.log('[applyGeneratedCode] Response data:', data); + console.log('[applyGeneratedCode] Debug info:', data.debug); + console.log('[applyGeneratedCode] Current sandboxData:', sandboxData); + console.log('[applyGeneratedCode] Current iframe element:', iframeRef.current); + console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current?.src); + + if (results.filesCreated?.length > 0) { + setConversationContext(prev => ({ + ...prev, + appliedCode: [...prev.appliedCode, { + files: results.filesCreated, + timestamp: new Date() + }] + })); + + // Update the chat message to show success + // Only show file list if not in edit mode + if (isEdit) { + addChatMessage(`Edit applied successfully!`, 'system'); + } else { + // Check if this is part of a generation flow (has recent AI recreation message) + const recentMessages = chatMessages.slice(-5); + const isPartOfGeneration = recentMessages.some(m => + m.content.includes('AI recreation generated') || + m.content.includes('Code generated') + ); + + // Don't show files if part of generation flow to avoid duplication + if (isPartOfGeneration) { + addChatMessage(`Applied ${results.filesCreated.length} files successfully!`, 'system'); + } else { + addChatMessage(`Applied ${results.filesCreated.length} files successfully!`, 'system', { + appliedFiles: results.filesCreated + }); + } + } + + // If there are failed packages, add a message about checking for errors + if (results.packagesFailed?.length > 0) { - addChatMessage(`⚠️ Some packages failed to install. Check the error banner above for details.`, 'system'); + + addChatMessage(`✓ Some packages failed to install. Check the error banner above for details.`, 'system'); + } + + // Fetch updated file structure + await fetchSandboxFiles(); + + // Automatically check and install any missing packages + await checkAndInstallPackages(); + + // Test build to ensure everything compiles correctly + // Skip build test for now - it's causing errors with undefined activeSandbox + // The build test was trying to access global.activeSandbox from the frontend, + // but that's only available in the backend API routes + console.log('[build-test] Skipping build test - would need API endpoint'); + + // Force iframe refresh after applying code + const refreshDelay = appConfig.codeApplication.defaultRefreshDelay; // Allow Vite to process changes + + setTimeout(() => { + if (iframeRef.current && sandboxData?.url) { + console.log('[home] Refreshing iframe after code application...'); + + // Method 1: Change src with timestamp + const urlWithTimestamp = `${sandboxData.url}?t=${Date.now()}&applied=true`; + iframeRef.current.src = urlWithTimestamp; + + // Method 2: Force reload after a short delay + setTimeout(() => { + try { + if (iframeRef.current?.contentWindow) { + iframeRef.current.contentWindow.location.reload(); + console.log('[home] Force reloaded iframe content'); + } + } catch (e) { + console.log('[home] Could not reload iframe (cross-origin):', e); + } + }, 1000); + } + }, refreshDelay); + + // Vite error checking removed - handled by template setup + } + + // Give Vite HMR a moment to detect changes, then ensure refresh + if (iframeRef.current && sandboxData?.url) { + // Wait for Vite to process the file changes + // If packages were installed, wait longer for Vite to restart + const packagesInstalled = results?.packagesInstalled?.length > 0 || data.results?.packagesInstalled?.length > 0; + const refreshDelay = packagesInstalled ? appConfig.codeApplication.packageInstallRefreshDelay : appConfig.codeApplication.defaultRefreshDelay; + console.log(`[applyGeneratedCode] Packages installed: ${packagesInstalled}, refresh delay: ${refreshDelay}ms`); + + setTimeout(async () => { + if (iframeRef.current && sandboxData?.url) { + console.log('[applyGeneratedCode] Starting iframe refresh sequence...'); + console.log('[applyGeneratedCode] Current iframe src:', iframeRef.current.src); + console.log('[applyGeneratedCode] Sandbox URL:', sandboxData.url); + + // Method 1: Try direct navigation first + try { + const urlWithTimestamp = `${sandboxData.url}?t=${Date.now()}&force=true`; + console.log('[applyGeneratedCode] Attempting direct navigation to:', urlWithTimestamp); + + // Remove any existing onload handler + iframeRef.current.onload = null; + + // Navigate directly + iframeRef.current.src = urlWithTimestamp; + + // Wait a bit and check if it loaded + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Try to access the iframe content to verify it loaded + try { + const iframeDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow?.document; + if (iframeDoc && iframeDoc.readyState === 'complete') { + console.log('[applyGeneratedCode] Iframe loaded successfully'); + return; + } + } catch (e) { + console.log('[applyGeneratedCode] Cannot access iframe content (CORS), assuming loaded'); + return; + } + } catch (e) { + console.error('[applyGeneratedCode] Direct navigation failed:', e); + } + + // Method 2: Force complete iframe recreation if direct navigation failed + console.log('[applyGeneratedCode] Falling back to iframe recreation...'); + const parent = iframeRef.current.parentElement; + const newIframe = document.createElement('iframe'); + + // Copy attributes + newIframe.className = iframeRef.current.className; + newIframe.title = iframeRef.current.title; + newIframe.allow = iframeRef.current.allow; + // Copy sandbox attributes + const sandboxValue = iframeRef.current.getAttribute('sandbox'); + if (sandboxValue) { + newIframe.setAttribute('sandbox', sandboxValue); + } + + // Remove old iframe + iframeRef.current.remove(); + + // Add new iframe + newIframe.src = `${sandboxData.url}?t=${Date.now()}&recreated=true`; + parent?.appendChild(newIframe); + + // Update ref + (iframeRef as any).current = newIframe; + + console.log('[applyGeneratedCode] Iframe recreated with new content'); + } else { + console.error('[applyGeneratedCode] No iframe or sandbox URL available for refresh'); + } + }, refreshDelay); // Dynamic delay based on whether packages were installed + } + + } else { + throw new Error(finalData?.error || 'Failed to apply code'); + } + } else { + // If no final data was received, still close loading + addChatMessage('Code application may have partially succeeded. Check the preview.', 'system'); + } + } catch (error: any) { + log(`Failed to apply code: ${error.message}`, 'error'); + } finally { + setLoading(false); + // Clear isEdit flag after applying code + setGenerationProgress(prev => ({ + ...prev, + isEdit: false + })); + } + }; + + const fetchSandboxFiles = async () => { + if (!sandboxData) return; + + try { + const response = await fetch('/api/get-sandbox-files', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + setSandboxFiles(data.files || {}); + setFileStructure(data.structure || ''); + console.log('[fetchSandboxFiles] Updated file list:', Object.keys(data.files || {}).length, 'files'); + } + } + } catch (error) { + console.error('[fetchSandboxFiles] Error fetching files:', error); + } + }; + + const restartViteServer = async () => { + try { + addChatMessage('Restarting Vite dev server...', 'system'); + + const response = await fetch('/api/restart-vite', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + addChatMessage('✓ Vite dev server restarted successfully!', 'system'); + + // Refresh the iframe after a short delay + setTimeout(() => { + if (iframeRef.current && sandboxData?.url) { + iframeRef.current.src = `${sandboxData.url}?t=${Date.now()}`; + } + }, 2000); + } else { + addChatMessage(`Failed to restart Vite: ${data.error}`, 'error'); + } + } else { + addChatMessage('Failed to restart Vite server', 'error'); + } + } catch (error) { + console.error('[restartViteServer] Error:', error); + addChatMessage(`Error restarting Vite: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); + } + }; + + const applyCode = async () => { + const code = promptInput.trim(); + if (!code) { + log('Please enter some code first', 'error'); + addChatMessage('No code to apply. Please generate code first.', 'system'); + return; + } + + // Prevent double clicks + if (loading) { + console.log('[applyCode] Already loading, skipping...'); + return; + } + + // Determine if this is an edit based on whether we have applied code before + const isEdit = conversationContext.appliedCode.length > 0; + await applyGeneratedCode(code, isEdit); + }; + + const renderMainContent = () => { + if (activeTab === 'generation' && (generationProgress.isGenerating || generationProgress.files.length > 0)) { + return ( + /* Generation Tab Content */ +
+ {/* File Explorer - Hide during edits */} + {!generationProgress.isEdit && ( +
+
+
+ + Explorer +
+
+ + {/* File Tree */} +
+
+ {/* Root app folder */} +
toggleFolder('app')} + > + {expandedFolders.has('app') ? ( + + ) : ( + + )} + {expandedFolders.has('app') ? ( + + ) : ( + + )} + app +
+ + {expandedFolders.has('app') && ( +
+ {/* Group files by directory */} + {(() => { + const fileTree: { [key: string]: Array<{ name: string; edited?: boolean }> } = {}; + + // Create a map of edited files + const editedFiles = new Set( + generationProgress.files - .filter(f => f.edited) + + .filter(f => f.completed) + .map(f => f.path) + ); + + // Process all files from generation progress + generationProgress.files.forEach(file => { + const parts = file.path.split('/'); + const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : ''; + const fileName = parts[parts.length - 1]; + + if (!fileTree[dir]) fileTree[dir] = []; + fileTree[dir].push({ - name: fileName, - edited: file.edited || false + + name: fileName + }); + }); + + return Object.entries(fileTree).map(([dir, files]) => ( +
+ {dir && ( +
toggleFolder(dir)} + > + {expandedFolders.has(dir) ? ( + + ) : ( + + )} + {expandedFolders.has(dir) ? ( + + ) : ( + + )} + {dir.split('/').pop()} +
+ )} + {(!dir || expandedFolders.has(dir)) && ( +
+ {files.sort((a, b) => a.name.localeCompare(b.name)).map(fileInfo => { + const fullPath = dir ? `${dir}/${fileInfo.name}` : fileInfo.name; + const isSelected = selectedFile === fullPath; + + return ( +
handleFileClick(fullPath)} + > + {getFileIcon(fileInfo.name)} + + {fileInfo.name} + {fileInfo.edited && ( + + )} + +
+ ); + })} +
+ )} +
+ )); + })()} +
+ )} +
+
+
+ )} + + {/* Code Content */} +
+ {/* Thinking Mode Display - Only show during active generation */} + {generationProgress.isGenerating && (generationProgress.isThinking || generationProgress.thinkingText) && ( +
+
+
+ {generationProgress.isThinking ? ( + <> +
- AI is thinking... + + Думаю, как лучше помочь... + + ) : ( + <> - + + Что дальше? + Thought for {generationProgress.thinkingDuration || 0} seconds + + )} +
+
+ {generationProgress.thinkingText && ( +
+
+
                       {generationProgress.thinkingText}
+
                     
+
+ )} +
+ )} + + {/* Live Code Display */} +
+
+ {/* Show selected file if one is selected */} + {selectedFile ? ( +
+
+
+
+ {getFileIcon(selectedFile)} + {selectedFile} +
+ +
+
+ { + const ext = selectedFile.split('.').pop()?.toLowerCase(); + if (ext === 'css') return 'css'; + if (ext === 'json') return 'json'; + if (ext === 'html') return 'html'; + return 'jsx'; + })()} + style={vscDarkPlus} + customStyle={{ + margin: 0, + padding: '1rem', + fontSize: '0.875rem', + background: 'transparent', + }} + showLineNumbers={true} + > + {(() => { + // Find the file content from generated files + const file = generationProgress.files.find(f => f.path === selectedFile); + return file?.content || '// File content will appear here'; + })()} + +
+
+
+ ) : /* If no files parsed yet, show loading or raw stream */ + generationProgress.files.length === 0 && !generationProgress.currentFile ? ( + generationProgress.isThinking ? ( + // Beautiful loading state while thinking +
+
+
+
+
+
+
+
-

AI is analyzing your request

-

{generationProgress.status || 'Preparing to generate code...'}

+ +

ИИ анализирует ваш запрос

+ +

{generationProgress.status || 'Подготовка к генерации кода...'}

+
+
+ ) : ( +
+
+
+
- Streaming code... + + Потоковая генерация кода... +
+
+
+ - {generationProgress.streamedCode || 'Starting code generation...'} + + {generationProgress.streamedCode || 'Начало генерации кода...'} + + +
+
+ ) + ) : ( +
+ {/* Show current file being generated */} + {generationProgress.currentFile && ( +
+
+
+
+ {generationProgress.currentFile.path} + + {generationProgress.currentFile.type === 'javascript' ? 'JSX' : generationProgress.currentFile.type.toUpperCase()} + +
+
+
+ + {generationProgress.currentFile.content} + + +
+
+ )} + + {/* Show completed files */} + {generationProgress.files.map((file, idx) => ( +
+
+
+ + {file.path} +
+ + {file.type === 'javascript' ? 'JSX' : file.type.toUpperCase()} + +
+
+ + {file.content} + +
+
+ ))} + + {/* Show remaining raw stream if there's content after the last file */} + {!generationProgress.currentFile && generationProgress.streamedCode.length > 0 && ( +
+
+
+
+ Processing... +
+
+
+ + {(() => { + // Show only the tail of the stream after the last file + const lastFileEnd = generationProgress.files.length > 0 + ? generationProgress.streamedCode.lastIndexOf('') + 7 + : 0; + let remainingContent = generationProgress.streamedCode.slice(lastFileEnd).trim(); + + // Remove explanation tags and content + remainingContent = remainingContent.replace(/[\s\S]*?<\/explanation>/g, '').trim(); + + // If only whitespace or nothing left, show waiting message + return remainingContent || 'Waiting for next file...'; + })()} + +
+
+ )} +
+ )} +
+
+ + {/* Progress indicator */} + {generationProgress.components.length > 0 && ( +
+
+
+
+
+ )} +
+
+ ); + } else if (activeTab === 'preview') { + // Show screenshot when we have one and (loading OR generating OR no sandbox yet) + if (urlScreenshot && (loading || generationProgress.isGenerating || !sandboxData?.url || isPreparingDesign)) { + return ( +
+ Website preview + {(generationProgress.isGenerating || isPreparingDesign) && ( +
+
+
+

+ {generationProgress.isGenerating ? 'Generating code...' : `Preparing your design for ${targetUrl}...`} +

+
+
+ )} +
+ ); + } + + // Check loading stage FIRST to prevent showing old sandbox + // Don't show loading overlay for edits + if (loadingStage || (generationProgress.isGenerating && !generationProgress.isEdit)) { + return ( +
+
+
+
+
+

- {loadingStage === 'gathering' && 'Gathering website information...'} - {loadingStage === 'planning' && 'Planning your design...'} - {(loadingStage === 'generating' || generationProgress.isGenerating) && 'Generating your application...'} + + {loadingStage === 'gathering' && 'Сбор информации о веб-сайте...'} + + {loadingStage === 'planning' && 'Планирование вашего дизайна...'} + + {(loadingStage === 'generating' || generationProgress.isGenerating) && 'Генерация вашего приложения...'} +

+

- {loadingStage === 'gathering' && 'Analyzing the website structure and content'} - {loadingStage === 'planning' && 'Creating the optimal React component architecture'} - {(loadingStage === 'generating' || generationProgress.isGenerating) && 'Writing clean, modern code for your app'} + + {loadingStage === 'gathering' && 'Анализ структуры и содержания веб-сайта'} + + {loadingStage === 'planning' && 'Создание оптимальной архитектуры React компонентов'} + + {(loadingStage === 'generating' || generationProgress.isGenerating) && 'Написание чистого, современного кода для вашего приложения'} +

+
+
+ ); + } + + // Show sandbox iframe only when not in any loading state + if (sandboxData?.url && !loading) { + return ( +
+