D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Авторство: hackeryaroslav
Источник: xss.is
Введение
Сегодня мы создадим простое, но интересное приложение, которое объединяет возможности искусственного интеллекта и кастомные настройки для суммирования текстов и ссылок. В качестве основы используем React, а именно его популярный фреймворк — Next.js. Мы сосредоточимся на интеграции ИИ API с React, чтобы продемонстрировать, как эти технологии могут эффективно работать вместе. Код будет объяснен подробно, и я расскажу о ключевых аспектах технологий, которые мы будем использовать.
Next.js: Мощный фреймворк для React-приложений
Next.js — это фреймворк для разработки веб-приложений на React, созданный компанией Vercel (тот самый хостинг, где я захостил 90% своих проектов).Благодаря своей гибкости и богатому функционалу, Next.js стал выбором многих разработчиков и компаний по всему миру.
Одной из ключевых особенностей Next.js является поддержка серверного рендеринга (SSR). Функция позволяет улучшить производительность и SEO приложения, делая его более привлекательным как для пользователей, так и для поисковых систем. Кроме того, Next.js предлагает возможность статической генерации (SSG), что позволяет создавать статические сайты, которые можно легко развернуть на CDN для максимальной производительности.

Разработчики особенно ценят интуитивно понятную систему маршрутизации Next.js, основанную на структуре файлов и папок. Фреймворк также автоматически оптимизирует ваше приложение, разделяя код на небольшие части для более быстрой загрузки, что улучшает пользовательский опыт. Next.js предоставляет возможность создавать серверные API прямо внутри вашего приложения, что удобно для разработки полноценных веб-приложений с серверной логикой. Работа со стилями также не вызывает трудностей благодаря встроенной поддержке CSS, включая CSS модули и Sass.
Особого внимания заслуживает функция автоматической оптимизации изображений. Next.js предлагает адаптивную загрузку изображений, что существенно улучшает производительность сайта, особенно на мобильных устройствах. (чуть позже объясню подробнее как)
Next.js: Серверная и клиентская сторона
Для лучшего понимания, мы погрузимся в глубь и рассмотрим как работает система рендеринга, которая включает в себя серверный рендеринг (SSR), статическую генерацию (SSG) и клиентский рендеринг (CSR). Дополнительно расскажу про гибридный подход и оптимизацию. Поехали.
Серверный рендеринг (SSR)
Next.js генерирует HTML на сервере для каждого запроса. Процесс выглядит следующим образом:
- Когда запрос поступает на сервер, Next.js вызывает функцию getServerSideProps (если она определена) для данной страницы.
- Сервер выполняет код React, преобразуя компоненты в HTML-строку. Это происходит с использованием ReactDOMServer.renderToString() или аналогичных методов.
- Сгенерированный HTML отправляется клиенту вместе с минимальным JavaScript (включая React и гидратационный код).
- На клиенте происходит "гидратация" — процесс, при котором React привязывает обработчики событий к уже существующему DOM, делая страницу интерактивной.
Статическая генерация (SSG)
Статическая генерация происходит во время сборки:- Next.js вызывает getStaticProps и getStaticPaths (для динамических маршрутов) для всех страниц, помеченных как статические.
- На основе полученных данных, Next.js предварительно рендерит каждую страницу в HTML.
- Сгенерированные файлы (HTML, JSON с пропсами, JavaScript) сохраняются и могут быть распределены через CDN.
- При запросе клиент получает готовый HTML и минимальный JavaScript для гидратации.
Клиентский рендеринг (CSR)
Клиентский рендеринг в Next.js обычно используется для динамических компонентов внутри страниц:- Сервер отправляет базовый HTML-скелет страницы и полный JavaScript-бандл.
- На клиенте React запускается и рендерит компоненты, заполняя или заменяя содержимое DOM.
- Для оптимизации производительности Next.js использует автоматическое разделение кода (code splitting), отправляя только необходимый JavaScript для каждой страницы.
Гибридный подход
Next.js позволяет комбинировать эти подходы:- Части страницы могут быть предварительно отрендерены на сервере, а другие части могут быть отрендерены на клиенте.
- Используется концепция "островов" (islands architecture), где статический контент окружает интерактивные "острова" динамического контента.
Оптимизация и кэширование
Next.js реализует сложные стратегии кэширования:- Автоматическое кэширование статических активов.
- Поддержка кэширования на уровне CDN для статических и ISR страниц.
- Возможность настройки заголовков кэширования для точного контроля над тем, как и когда контент обновляется.
Оптимизация изображений в Next.js
Одна из фишек Next.js это мощный инструмент для оптимизации изображений через компонент next/image. Этот компонент предлагает ряд автоматических оптимизаций, которые значительно улучшают производительность и пользовательский опыт. Способы приведены ниже.
Автоматическое изменение размера
Компонент Image автоматически изменяет размер изображений на сервере. Когда вы указываете width и height пропсы, Next.js генерирует изображение нужного размера во время сборки (для статических изображений) или по запросу (для динамических изображений).JavaScript: Скопировать в буфер обмена
Код:
import Image from 'next/image'
function MyComponent() {
return (
<Image
src="/image.jpg"
width={500}
height={300}
alt="Description"
/>
)
}
Адаптивная загрузка
Также, Next.js использует технику "ленивой загрузки" (lazy loading) по умолчанию. Изображения загружаются только когда они попадают в область видимости браузера. Это значительно ускоряет начальную загрузку страницы. (тут подробнее)Кроме того, Next.js автоматически генерирует и обслуживает изображения в современных форматах, таких как WebP, если браузер их поддерживает.
Placeholder и размытие
Компонент Image поддерживает различные стратегии для отображения placeholder'ов, пока изображение загружается:JavaScript: Скопировать в буфер обмена
Код:
<Image
src="/my-image.jpg"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/........"
/>
Оптимизация для Web Vitals
Компонент Image автоматически устанавливает атрибут sizes, что помогает браузеру выбрать правильное изображение для текущего размера экрана. Это улучшает метрику Largest Contentful Paint (LCP).CDN и кэширование
И последнее, Next.js позволяет настраивать CDN для обслуживания оптимизированных изображений. Вы можете указать домен или префикс пути для изображений, хранящихся на внешнем CDN:JavaScript: Скопировать в буфер обмена
Код:
module.exports = {
images: {
domains: ['example.com'],
path: 'https://example.com/images',
},
}
Файловая система как маршрутизатор в Next.js
Next.js использует интересный подход к маршрутизации, основанный на структуре файловой системы. Это упрощает создание и поддержку маршрутов в приложении. Как? Давайте взглянем.Основы файловой маршрутизации
В Next.js каждый файл внутри директории pages автоматически становится маршрутом. Например:- pages/index.js → /
- pages/about.js → /about
- pages/contact.js → /contact
Вложенные маршруты
Для создания вложенных маршрутов, просто создайте соответствующую структуру папок:- pages/blog/index.js → /blog
- pages/blog/first-post.js → /blog/first-post
- pages/blog/second-post.js → /blog/second-post
Динамические маршруты
Next.js поддерживает динамические сегменты в маршрутах. Для этого используются квадратные скобки:- pages/posts/[id].js → /posts/1, /posts/2, и т.д.
JavaScript: Скопировать в буфер обмена
Код:
import { useRouter } from 'next/router'
function Post() {
const router = useRouter()
const { id } = router.query
return <p>Post: {id}</p>
}
export default Post
Catch-all маршруты
Для обработки множества сегментов пути используются троеточия внутри скобок:- pages/posts/[...slug].js → /posts/2020/01/01, /posts/category/subcategory/post, и т.д.
Приоритет маршрутов
Next.js имеет четкую систему приоритетов для разрешения маршрутов:- Предопределенные маршруты (/about.js)
- Динамические маршруты (/[id].js)
- Catch-all маршруты (/[...slug].js)
API маршруты
Файловая система также используется для создания API маршрутов. Файлы в директории pages/api автоматически становятся API эндпоинтами:- pages/api/users.js → /api/users
JavaScript: Скопировать в буфер обмена
Код:
export default function handler(req, res) {
res.status(200).json({ name: 'DarkBoys666 })
}
SSR и кэширование в Next.js
Эту тему я оставил на последок, так как она может показаться несколько сложной. Но, Next.js предоставляет несколько способов контроля над кэшированием, включая использование HTTP-заголовков и кэширование на уровне CDN.
Управление кэшированием через HTTP-заголовки
Next.js позволяет устанавливать HTTP-заголовки кэширования для SSR-страниц. Это можно сделать в функции getServerSideProps:JavaScript: Скопировать в буфер обмена
Код:
export async function getServerSideProps({ res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
)
// Получение данных...
return { props: { /* данные */ } }
}
Разберем заголовок Cache-Control:
- public: Разрешает кэширование ответа как промежуточными прокси, так и браузером.
- s-maxage=10: Устанавливает TTL (время жизни) кэша на уровне CDN на 10 секунд.
- stale-while-revalidate=59: Позволяет использовать устаревший кэш еще 59 секунд, пока происходит фоновое обновление.
Кэширование на уровне CDN
Next.js отлично работает с CDN, что позволяет кэшировать ответы SSR близко к пользователю. Для эффективного использования CDN:- Настройте CDN для уважения заголовков Cache-Control.
- Используйте стратегию stale-while-revalidate для баланса между свежестью данных и скоростью.
- Рассмотрите использование getStaticProps с revalidate для страниц, которые могут быть предварительно отрендерены.
Код:
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // Регенерация страницы каждые 60 секунд
}
}
Инвалидация кэша
Для ситуаций, когда нужно немедленно обновить кэшированный контент, Next.js предоставляет API для инвалидации кэша:JavaScript: Скопировать в буфер обмена
Код:
// pages/api/revalidate.js
export default async function handler(req, res) {
await res.revalidate('/path-to-revalidate')
return res.json({ revalidated: true })
}
Условное кэширование
Иногда нужно кэшировать ответы на основе определенных условий. Это можно сделать, динамически устанавливая заголовки в getServerSideProps:export async function getServerSideProps({ req, res }) {
const userAgent = req.headers['user-agent']
JavaScript: Скопировать в буфер обмена
Код:
if (userAgent.includes('Googlebot')) {
res.setHeader('Cache-Control', 's-maxage=3600')
} else {
res.setHeader('Cache-Control', 's-maxage=60')
}
// ...
}
Отлично. Теперь, когда мы поняли как работает движок NextJS мы готовы начать разрабатывать наше приложение. Приложение выполняет анализ текста, позволяя пользователям вводить URL или вставлять текст для обработки. Основные функции включают: краткое содержание статьи с выделением ключевых моментов, анализ настроений (позитивные, негативные, нейтральные), определение ключевых тем и сущностей (имена, места). Интерактивные элементы позволяют настраивать уровень сжатия и детализации анализа, а графики визуализируют тренды и данные. Дополнительные возможности: оценка читаемости, выявление предвзятости, предложения по проверке фактов и классификация контента по темам.
Над дизайном мы тоже поработаем. Только посмотрите на это:
Также у нас есть главная страница, с анимированным бэкграундом. Простая, но не менее интересная. Мини видео тут:
Начинаем
Начнем со структуры проекта:
Код: Скопировать в буфер обмена
Код:
project:
|-- .env.local
|-- components.json
|-- jsconfig.json
|-- next.config.mjs
|-- package.json
|-- postcss.config.mjs
|-- tailwind.config.js
|-- lib
|-- utils.js
|-- public
|-- favicon.ico
|-- styles
|-- globals.css
|-- components
|-- ui
|-- badge.jsx
|-- button.jsx
|-- card.jsx
|-- input.jsx
|-- slider.jsx
|-- tabs.jsx
|-- pages
|-- AINewsAnalyzer.js
|-- index.js
|-- _app.js
|-- _document.js
|-- api
|-- analyze.js
|-- fonts
|-- GeistMonoVF.woff
|-- GeistVF.woff
Давайте начнем разбор нашего проекта с файла analyze.js, который является ключевым компонентом, отвечающим за анализ текста с использованием ИИ-модели и его обработки. Этот файл выполняет функции анализа новостных статей и определения их тональности, ключевых тем и прочих характеристик. Рассмотрим его более детально. Как и во многих моих статьях, мы используем Gemini 1.5 flash от Гугл -
https://ai.google.dev/gemini-api/docs?hl=ru
Импорт библиотек и инициализация модели
В начале файла мы импортируем необходимые модули и инициализируем модель ИИ от Google:
JavaScript: Скопировать в буфер обмена
Код:
import { GoogleGenerativeAI } from "@google/generative-ai";
import puppeteer from "puppeteer";
puppeteer — мощный инструмент для автоматизации браузера, который мы используем для извлечения текста с веб-страниц, если вместо текста пользователь предоставляет URL.
После этого инициализируется модель:
JavaScript: Скопировать в буфер обмена
Код:
const genAI = new GoogleGenerativeAI(process.env.API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
Здесь создается экземпляр модели gemini-1.5-flash с использованием API-ключа, который загружается из файла окружения (.env.local). Эта модель отвечает за обработку и анализ текста.
Вспомогательные функции
Далее мы определяем две важные функции: isUrl и extractTextFromUrl.JavaScript: Скопировать в буфер обмена
Код:
const isUrl = (string) => {
const pattern = new RegExp(
"^(https?:\\/\\/)?" +
"((([a-z0-9][-a-z0-9]*[a-z0-9])?\\.)+[a-z]{2,}|" +
"localhost|" +
"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
"\\[([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}\\]|" +
"([0-9a-f]{1,4}:){1,7}:|" +
"([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|" +
"([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|" +
"([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|" +
"([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|" +
"([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|" +
"[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|" +
":((:[0-9a-f]{1,4}){1,7}|:)|" +
"fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" +
"::(ffff(:0{1,4}){0,1}:){0,1}" +
"(25[0-5]|(2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))" +
"(\\.(25[0-5]|(2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))){3})" +
"(\\:[0-9]+)?(\\/.*)?$",
"i"
);
return !!pattern.test(string);
};
};
isUrl проверяет, является ли входная строка URL-адресом, для правильной обработки пользовательского ввода. Если это URL, текст будет извлекаться с веб-страницы, если же это простой текст, анализ будет выполнен напрямую.
JavaScript: Скопировать в буфер обмена
Код:
const extractTextFromUrl = async (url) => {
try {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
await page.waitForSelector("#mw-content-text");
const bodyText = await page.evaluate(() => {
return document.querySelector("#mw-content-text").innerText;
});
await browser.close();
return bodyText;
} catch (error) {
console.error("Error extracting text from URL:", error);
throw error;
}
};
extractTextFromUrl запускает безголовый браузер через puppeteer для извлечения текста с веб-страницы, указанной по URL. Эта функция полезна, когда пользователь предоставляет ссылку на статью, а не её текст. Мы автоматически заходим на эту страницу, ждем, пока загрузится основной контент, и извлекаем текст.
Основная логика обработчика запросов
Теперь давайте рассмотрим саму функцию-обработчик, которая вызывается, когда API получает запрос на анализ текста:JavaScript: Скопировать в буфер обмена
Код:
export default async function handler(req, res) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const { text, conciseness, detailLevel } = req.body;
if (!text) {
return res.status(400).json({ message: "Text is required" });
}
try {
let inputText = text;
if (isUrl(text)) {
console.log("Fetching URL:", text);
inputText = await extractTextFromUrl(text);
console.log("Extracted text:", inputText);
}
- В самом начале мы проверяем, чтобы метод запроса был POST, так как GET запросы не поддерживаются этим API.
- Далее извлекаем текст и параметры, такие как conciseness (степень краткости) и detailLevel (уровень детализации), которые пользователь может передавать для настройки выводимой информации.
- Если в запросе содержится URL, текст будет извлекаться с помощью функции extractTextFromUrl.
JavaScript: Скопировать в буфер обмена
Код:
const requestBody = {
contents: [
{
parts: [
{
text: `Analyze the following text and provide:
1. A summary (conciseness: ${conciseness}/100, detail level: ${detailLevel}/100)
2. Sentiment analysis (positive, neutral, or negative with a score from 0 to 100)
3. Key topics with relevance scores
4. Named entities (people, organizations, locations, etc.)
5. Readability score (0-100)
6. Bias detection (left-leaning, center, right-leaning)
7. Fact check suggestions
8. Content classification
Text to analyze:
${inputText}
Format the response as JSON with the following structure:
{
"summary": "...",
"sentiment": {
"label": "positive/neutral/negative",
"score": 0-100,
"emotions": [{"label": "...", "color": "..."}]
},
"topics": [{"name": "...", "relevance": 0-1}],
"entities": [{"type": "...", "examples": ["...", "..."]}],
"readability": {
"score": 0-100,
"description": "..."
},
"bias": {
"label": "left-leaning/center/right-leaning",
"description": "..."
},
"factCheckSuggestions": ["...", "..."],
"contentClassification": ["...", "..."]
};`,
},
],
},
],
};
Здесь мы создаем тело запроса для ИИ, которое включает параметры анализа текста:
- Краткий пересказ текста с учетом указанных параметров conciseness и detailLevel.
- Анализ тональности (позитивная, нейтральная или негативная) с оценкой от 0 до 100.
- Ключевые темы и их релевантность.
- Именованные сущности (люди, организации, места).
- Оценка читаемости текста.
- Определение политической предвзятости (левоцентристская, нейтральная или правоцентристская).
- Рекомендации по проверке фактов.
- Классификация контента.
Обработка ответа и финальные действия
Когда ответ от ИИ получен, он обрабатывается и форматируется:JavaScript: Скопировать в буфер обмена
Код:
const result = await model.generateContent(requestBody);
const responseText = await result.response.text();
const cleanedText = responseText
.replace(/```json/, "")
.replace(/```/, "")
.trim();
Мы убираем лишние символы форматирования, такие как блоки кода, и конвертируем результат в JSON:
JavaScript: Скопировать в буфер обмена
Код:
const parsedResult = JSON.parse(cleanedText);
parsedResult.sentiment.emotions = parsedResult.sentiment.emotions.map(
(emotion) => {
const colorMap = {
joy: "yellow",
sadness: "blue",
anger: "red",
fear: "purple",
surprise: "orange",
disgust: "green",
trust: "teal",
anticipation: "pink",
};
return {
...emotion,
color: colorMap[emotion.label.toLowerCase()] || "gray",
};
}
);
parsedResult.topics = parsedResult.topics.slice(0, 5);
Мы добавляем цвета для эмоций и сокращаем список ключевых тем до пяти наиболее релевантных.
В конце файл возвращает JSON с результатами анализа:
JavaScript: Скопировать в буфер обмена
return res.status(200).json(parsedResult);
Поздравляю! Мы прошли самую сложную часть проекта — логику ИИ. Осталось только построить дизайн клиента и отображать результаты.
Но перед тем как начнем, нужно понять, как мы будем строить наши страницы. А именно, поговорим об shadcn — позиционирует себя не как очередная библиотека компонентов, а именно коллекцию из компонентов, которых можно скопировать и вставить. Почему? Дать полный контроль над компонентами. Официальная документация доступна здесь: https://ui.shadcn.com/docs
Пропишем парочку команд, которые добавят необходимые компоненты в проект:
Код: Скопировать в буфер обмена
Код:
npx shadcn-ui@latest add input
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add slider
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add tabs
Перейдем к нашей лэндинг странице:
JavaScript: Скопировать в буфер обмена
Код:
import { useState } from 'react';
import Head from 'next/head';
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Zap, Network, Layers, Atom } from "lucide-react";
import AINewsAnalyzer from './AINewsAnalyzer';
Этот блок импортирует необходимые модули. useState — это хук React, который управляет состоянием компонента, Head из Next.js отвечает за метаданные страницы (например, <title>, <meta> и <link>), а motion из библиотеки Framer Motion используется для анимаций. Иконки из lucide-react добавляют визуальные элементы в дизайн, а кнопка импортируется из пользовательских компонентов.
Компонент LandingPage
Основной компонент начинается с объявления состояния:JavaScript: Скопировать в буфер обмена
Код:
export default function LandingPage() {
const [isLaunched, setIsLaunched] = useState(false);
const handleLaunch = () => {
setIsLaunched(true);
};
Здесь isLaunched хранит информацию о том, была ли запущена главная функция — анализ новостей. Изначально она равна false, и при нажатии на кнопку через функцию handleLaunch это состояние меняется на true, что вызывает отображение другого контента (как раз таки нашего ИИ).
Разметка страницы и условная отрисовка
Далее идет основной JSX, где реализуется условная отрисовка интерфейса:JavaScript: Скопировать в буфер обмена
Код:
return (
<div className="min-h-screen bg-black text-white overflow-hidden">
<Head>
<title>AI News Analyzer</title>
<meta name="description" content="Decode the news landscape with quantum-inspired algorithms" />
<link rel="icon" href="/favicon.ico" />
</Head>
Блок <Head> управляет метаданными страницы. Внутри него задается заголовок страницы с помощью тега <title>, описание для поисковых систем через <meta> и иконка сайта через <link>.
Основная структура страницы заключена в контейнер с классами из Tailwind CSS: min-h-screen, bg-black, text-white. Это определяет минимальную высоту экрана, черный фон и белый текст. Ничего нового.
Условное отображение содержимого
Основная идея в том, что пока isLaunched равно false, показывается лендинг с кнопкой запуска:JavaScript: Скопировать в буфер обмена
Код:
{!isLaunched ? (
<>
<main className="relative z-10">
<section className="min-h-screen flex flex-col justify-center items-center px-4 text-center">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="max-w-4xl mx-auto"
>
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500">
Decode the News Landscape
</h1>
<p className="text-xl md:text-2xl mb-8 text-gray-300">
Harness quantum-inspired algorithms to unravel complex narratives
</p>
<Button size="lg" className="bg-blue-600 hover:bg-blue-700 text-white text-lg px-8 py-6" onClick={handleLaunch}>
Explore the Matrix
<Zap className="ml-2 h-5 w-5" />
</Button>
</motion.div>
</section>
Этот блок отображает заголовок, описание и кнопку, которая вызывает функцию handleLaunch. Класс motion.div из Framer Motion добавляет анимацию: при загрузке страницы блок плавно появляется на экране, начиная с опущенного положения (с помощью y: 50) и постепенно становится полностью видимым за 0.8 секунд.
Секции с функциональностями
Ниже представлены три "технические особенности", каждая из которых визуализирована с использованием компонентов:JavaScript: Скопировать в буфер обмена
Код:
<section className="py-20">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<TechFeature
icon={<Network className="h-12 w-12 text-blue-500" />}
title="Quantum Narrative Mapping"
description="Uncover hidden connections in global events"
/>
<TechFeature
icon={<Layers className="h-12 w-12 text-purple-500" />}
title="Hyperdimensional Data Synthesis"
description="Fuse multi-source intelligence for deeper insights"
/>
<TechFeature
icon={<Atom className="h-12 w-12 text-green-500" />}
title="Cognitive Spectrum Analysis"
description="Decode complex emotional landscapes in real-time"
/>
</div>
</div>
</section>
Каждая из функций (TechFeature) имеет заголовок, описание и иконку. Этот блок также анимируется при помощи Framer Motion. Он разбит на три части с использованием CSS-классов сетки.
Футер
Последний элемент лендинга — это футер:JavaScript: Скопировать в буфер обмена
Код:
<footer className="relative z-10 bg-gray-900/50 py-8 backdrop-blur-sm">
<div className="container mx-auto px-4 text-center text-gray-400 text-sm">
Empowering informed decisions through advanced AI
</div>
</footer>
Футер содержит текст, который поясняет миссию приложения, а класс backdrop-blur-sm добавляет размытие фона, делая его более легким визуально.
Анимации частиц
Завершает страницу анимация частиц, реализованная с помощью цикла по 100 элементов, где каждая частица создается и анимируется:JavaScript: Скопировать в буфер обмена
Код:
<div className="fixed inset-0 z-0 overflow-hidden">
<motion.div
className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-20"
animate={{
scale: [1, 1.5, 1],
rotate: [0, 360],
backgroundColor: ["#3b82f6", "#9333ea", "#ec4899"],
}}
transition={{
duration: 25,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-to-r from-green-500 to-pink-500 rounded-full opacity-20"
animate={{
scale: [1.3, 1, 1.3],
rotate: [360, 0],
backgroundColor: ["#10b981", "#f43f5e", "#3b82f6"],
}}
transition={{
duration: 25,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</div>
Здесь задается два анимированных элемента, которые плавно меняют масштаб и вращаются по кругу, добавляя динамику всей странице. Эти анимации создают эффект движения и привлекают внимание пользователя, добавляя современный вид. Пользователь заинтересован, страница выглядит живой.
Когда состояние isLaunched меняется на true, вместо лендинга с анимациями появляется основная часть приложения — компонент AINewsAnalyzer:
JavaScript: Скопировать в буфер обмена
Код:
) : (
<div className="min-h-screen flex justify-center items-center">
<AINewsAnalyzer />
</div>
)}
</div>
);
}
Главный файл
Файл AINewsAnalyzer.js представляет собой центральный компонент приложения. Этот файл является основой для работы с пользовательским вводом, вызова API и отображения результатов анализа.
Импорт библиотек и компонентов
В начале файла импортируются библиотеки и компоненты:JavaScript: Скопировать в буфер обмена
Код:
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Slider } from "@/components/ui/slider";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Zap, BarChart2, PieChart, BookOpen, Brain, Cpu } from "lucide-react";
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
Здесь подключаются основные библиотеки, такие как React и Framer Motion, для управления состояниями и анимациями. Компоненты пользовательского интерфейса — Input, Button, Card и другие — обеспечивают интерактивность приложения. lucide-react используется для добавления иконок, а recharts — для построения графиков.
Данные и состояния
Для обработки данных внутри компонента используется несколько состояний:JavaScript: Скопировать в буфер обмена
Код:
const [isExpanded, setIsExpanded] = useState(false);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [inputText, setInputText] = useState("");
const [summary, setSummary] = useState("");
const [sentiment, setSentiment] = useState(null);
const [topics, setTopics] = useState([]);
const [entities, setEntities] = useState([]);
const [conciseness, setConciseness] = useState(50);
const [detailLevel, setDetailLevel] = useState(50);
Эти состояния управляют поведением приложения. Несколько из них:
- inputText хранит текст статьи или ссылку, которую вводит пользователь.
- isAnalyzing контролирует процесс анализа текста.
- summary, sentiment, topics и другие хранят результаты анализа, такие как краткое содержание, тональность, ключевые темы и сущности.
Функция анализа текста
Главная функция analyzeText отвечает за отправку текста на сервер для анализа:JavaScript: Скопировать в буфер обмена
Код:
const analyzeText = async () => {
setIsAnalyzing(true);
try {
const response = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: inputText, conciseness, detailLevel }),
});
if (!response.ok) {
throw new Error("Failed to analyze text");
}
const data = await response.json();
setSummary(data.summary);
setSentiment(data.sentiment);
setTopics(data.topics);
setEntities(data.entities);
setReadability(data.readability);
setBias(data.bias);
setFactCheckSuggestions(data.factCheckSuggestions);
setContentClassification(data.contentClassification);
} catch (error) {
console.error("Error analyzing text:", error);
} finally {
setIsAnalyzing(false);
}
};
Функция отправляет данные на API и получает результат в формате JSON, после чего обновляет состояния, такие как краткое содержание, тональность и ключевые темы.
Интерфейс пользователя
Функция возвращает JSX, который описывает интерфейс приложения:JavaScript: Скопировать в буфер обмена
Код:
return (
<div className="min-h-screen bg-black text-white p-4 md:p-8 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-purple-900 via-gray-900 to-black"></div>
<AbstractShape className="absolute top-0 left-0 w-96 h-96 text-blue-500/10 animate-pulse" />
<AbstractShape className="absolute bottom-0 right-0 w-96 h-96 text-purple-500/10 animate-pulse" />
<motion.div
className="max-w-7xl mx-auto relative z-10"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<motion.h1
className="text-6xl font-bold mb-8 text-center bg-clip-text text-transparent bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500"
initial={{ y: -50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
NexusInsight AI
</motion.h1>
<Card className="bg-gray-900/30 border-gray-800 mb-8 backdrop-blur-sm">
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-4">
<Input
placeholder="Enter article URL or paste text"
className="bg-gray-800/50 border-gray-700 text-white flex-grow"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
<Button
className={`bg-blue-600 hover:bg-blue-700 transition-all duration-300 ${
isAnalyzing ? "animate-pulse" : ""
}`}
onClick={analyzeText}
disabled={isAnalyzing || !inputText.trim()}
>
{isAnalyzing ? "Analyzing" : "Analyze"}
<Zap className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
<Card className="bg-gray-900/30 border-gray-800 col-span-1 md:col-span-2 backdrop-blur-sm">
<CardContent className="p-6">
<h2 className="text-2xl font-bold mb-4 flex items-center">
<Brain className="mr-2" />
AI-Generated Summary
</h2>
<p className="text-gray-300 mb-4">
{summary ||
"Your article summary will appear here, providing key insights and main points extracted from the text."}
</p>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span>Conciseness</span>
<Slider
value={[conciseness]}
onValueChange={(value) => setConciseness(value[0])}
max={100}
step={1}
className="w-1/2"
/>
</div>
<div className="flex justify-between items-center">
<span>Detail Level</span>
<Slider
value={[detailLevel]}
onValueChange={(value) => setDetailLevel(value[0])}
max={100}
step={1}
className="w-1/2"
/>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/30 border-gray-800 backdrop-blur-sm">
<CardContent className="p-6">
<h2 className="text-2xl font-bold mb-4 flex items-center">
<Cpu className="mr-2" />
Sentiment Analysis
</h2>
{sentiment && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Badge
variant="outline"
className={`bg-${sentiment.color}-900/50 text-${sentiment.color}-300`}
>
{sentiment.label}
</Badge>
</div>
<div className="relative pt-1">
<div className="overflow-hidden h-2 text-xs flex rounded bg-gray-700">
<motion.div
className={`shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-${sentiment.color}-500`}
initial={{ width: "0%" }}
animate={{ width: `${sentiment.score}%` }}
transition={{ duration: 1, delay: 0.5 }}
/>
</div>
</div>
<div>
<h3 className="font-semibold mb-2">Dominant Emotions</h3>
<div className="flex flex-wrap gap-2">
{sentiment.emotions.map((emotion, index) => (
<Badge
key={index}
className={`bg-${emotion.color}-600`}
>
{emotion.label}
</Badge>
))}
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
<Card className="bg-gray-900/30 border-gray-800 mb-8 backdrop-blur-sm">
<CardContent className="p-6">
<Tabs defaultValue="topics">
<TabsList className="grid text-black w-full grid-cols-3 mb-4">
<TabsTrigger value="topics">
<PieChart className="mr-2 h-4 w-4" />
Key Topics
</TabsTrigger>
<TabsTrigger value="entities">
<BarChart2 className="mr-2 h-4 w-4" />
Named Entities
</TabsTrigger>
<TabsTrigger value="trends">
<TrendingUp className="mr-2 h-4 w-4" />
Trend Analysis
</TabsTrigger>
</TabsList>
<TabsContent value="topics">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{topics.map((topic, index) => (
<motion.div
key={index}
className="bg-gray-800/50 p-4 rounded-lg"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<h3 className="font-semibold mb-2">{topic.name}</h3>
<motion.div
className="w-full bg-gray-700 rounded-full h-2.5"
initial={{ width: 0 }}
animate={{ width: "100%" }}
transition={{ duration: 0.5, delay: 0.5 + index * 0.1 }}
>
<motion.div
className="bg-blue-600 h-2.5 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${topic.relevance * 100}%` }}
transition={{ duration: 0.5, delay: 1 + index * 0.1 }}
/>
</motion.div>
</motion.div>
))}
</div>
</TabsContent>
<TabsContent value="entities">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{entities.map((entity, index) => (
<motion.div
key={index}
className="bg-gray-800/50 p-4 rounded-lg"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<h3 className="font-semibold mb-2">{entity.type}</h3>
<ul className="list-disc list-inside text-sm text-gray-300">
{entity.examples.map((example, i) => (
<li key={i}>{example}</li>
))}
</ul>
</motion.div>
))}
</div>
</TabsContent>
<TabsContent value="trends">
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={dummyData}>
<XAxis dataKey="name" stroke="#888888" />
<YAxis stroke="#888888" />
<Tooltip />
<Line
type="monotone"
dataKey="value"
stroke="#8884d8"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Card className="bg-gray-900/30 border-gray-800 mb-8 backdrop-blur-sm">
<CardContent className="p-6">
<h2 className="text-2xl font-bold mb-4">Advanced Insights</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<motion.div
className="bg-gray-800/50 p-4 rounded-lg"
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.5 }}
>
<h3 className="font-semibold mb-2 flex items-center">
<BookOpen className="mr-2 h-4 w-4" />
Readability Score
</h3>
{readability && (
<>
<div className="flex items-center">
<div className="w-full bg-gray-700 rounded-full h-2.5 mr-2">
<motion.div
className="bg-green-600 h-2.5 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${readability.score}%` }}
transition={{ duration: 1 }}
/>
</div>
<span>{readability.score}/100</span>
</div>
<p className="text-sm text-gray-300 mt-2">
{readability.description}
</p>
</>
)}
</motion.div>
<motion.div
className="bg-gray-800/50 p-4 rounded-lg"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<h3 className="font-semibold mb-2 flex items-center">
<Scale className="mr-2 h-4 w-4" />
Bias Detection
</h3>
{bias && (
<>
<Badge
className={`bg-${
bias.label === "left-leaning"
? "blue"
: bias.label === "right-leaning"
? "red"
: "yellow"
}-600`}
>
{bias.label}
</Badge>
<p className="text-sm text-gray-300 mt-2">
{bias.description}
</p>
</>
)}
</motion.div>
<motion.div
className="bg-gray-800/50 p-4 rounded-lg"
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h3 className="font-semibold mb-2 flex items-center">
<CheckCircle className="mr-2 h-4 w-4" />
Fact Check Suggestions
</h3>
<ul className="list-disc list-inside text-sm text-gray-300">
{factCheckSuggestions.map((suggestion, index) => (
<li key={index}>{suggestion}</li>
))}
</ul>
</motion.div>
<motion.div
className="bg-gray-800/50 p-4 rounded-lg"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<h3 className="font-semibold mb-2 flex items-center">
<Tags className="mr-2 h-4 w-4" />
Content Classification
</h3>
<div className="flex flex-wrap gap-2">
{contentClassification.map((classification, index) => (
<Badge key={index} className="bg-purple-600">
{classification}
</Badge>
))}
</div>
</motion.div>
</div>
</CardContent>
</Card>
</motion.div>
)}
</AnimatePresence>
<motion.div
className="fixed bottom-8 right-8 bg-blue-600 rounded-full p-4 cursor-pointer z-50 hover:bg-blue-700 transition-colors duration-300"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<Minimize2 className="h-6 w-6" />
) : (
<Maximize2 className="h-6 w-6" />
)}
</motion.div>
</motion.div>
</div>
);
}
Заключение
В этой статье мы рассмотрели важные аспекты фреймворка Next.js — технологии, которая произвела настоящий фурор в разработке веб-приложений. Мы начали с анализа принципов работы фреймворка и его популярности среди разработчиков, после чего перешли к проекту, охватывающему ключевые темы: взаимодействие с API, применение ИИ, а также проектирование и реализацию логики.
Я детально разобрал лишь три файла, предоставляя вам возможность ознакомиться с остальными самостоятельно. Внизу вы найдете ZIP-архив проекта, который можно использовать для экспериментов со стилями и функционалом. Благодарю вас за внимание!