Рохан Кумар

Со всеми последними разработками в области искусственного интеллекта стало проще, чем когда-либо, создавать системы, которые могут предоставлять персонализированную обратную связь и рекомендации. Более того, с учетом того, насколько доступным Spotify сделал все данные о прослушивании своих пользователей, мы можем использовать ChatGPT и бесплатную общедоступную службу API Spotify для создания очень простого веб-сайта, который предоставляет достаточно точные рекомендации, основанные на вашей собственной истории прослушивания.

В этом посте мы рассмотрим полный процесс создания функционального веб-сайта, который будет выполнять следующие действия:

  1. Аутентифицируйте пользователя с помощью Spotify.
  2. Доступ и отображение самых популярных треков и исполнителей пользователя.
  3. Используйте принципы оперативной разработки для создания точных и эффективных ответов на рекомендации из модели OpenAI GPT-3.5-Turbo.
  4. Расшифруйте список песен, выведенных GPT-3.5-Turbo, и отправьте их обратно в Spotify, чтобы получить информацию о песне.
  5. Отобразите список рекомендуемых песен для пользователя вместе со ссылками для их прослушивания на Spotify.

Мы пренебрежем большей частью внешнего интерфейса и создадим простое приложение, которое можно редактировать и улучшать с помощью стилей CSS и других компонентов React.

Если вы хотите увидеть, как может выглядеть полностью разработанная версия этого проекта, загляните на мой сайт здесь.

Хотя это звучит как сложный проект, на самом деле его можно выполнить за удивительно небольшое количество строк кода.

Для начала нам сначала нужно настроить Spotify API. Мы делаем это, перейдя по следующей ссылке и настроив новое приложение. На данный момент большая часть введенной вами информации не имеет значения. Единственное, что имеет значение, это поле «URI перенаправления».

Когда пользователь нажимает кнопку входа на нашем веб-сайте, он будет перенаправлен на сайт в домене Spotify. Как только пользователь разрешает доступ к нашему веб-сайту через веб-сайт Spotify, он будет перенаправлен на наш URI перенаправления, что в идеале должно вернуть его на наш веб-сайт. На данный момент, поскольку мы работаем локально, мы можем установить его как «https://localhost:3000».

Следует отметить, что служба API Spotify на момент создания этого сообщения в блоге была полностью бесплатной, что делает ее идеальной для небольшого побочного проекта или чего-то еще в образовательных целях.

Как только мы инициализируем наше приложение с помощью Spotify, мы можем перейти к настройкам. Там мы увидим сверху два важных пункта: Client ID и App Status.

  1. Идентификатор клиента — это то, что мы будем использовать в качестве нашего ключа, позволяющего нам подключаться к службе аутентификации Spotify.
  2. Статус приложения, для которого по умолчанию должен быть установлен режим разработки, указывает, что веб-сайт позволит пройти аутентификацию только тем пользователям, которые вручную зарегистрировались для этого приложения. По умолчанию это будет включать только вашу личную учетную запись, которую вы использовали для создания приложения. Если вы хотите добавить нескольких друзей, вы можете вручную добавить их адреса электронной почты на вкладке «Управление пользователями». Но если вы хотите, чтобы ваш веб-сайт был общедоступным, вам нужно будет отправить запрос на расширение, который может занять до 6 недель для обработки Spotify.

Для начала мы инициализируем новый проект React. Лично мне нравится начинать с шаблона create-react-app, так как с ним проще всего работать, но подойдет любой шаблон. Для этого перейдите к терминалу и в каталог, где вы хотите, чтобы ваш проект находился, и введите

npx create-react-app song-recommender

Это инициализирует проект под названием «song-recommender» с кучей общих зависимостей/библиотек, что позволит нам сразу начать работу над нашим сайтом. Для начала набираем

npm start
npm i axios

Мы можем очистить проект, удалив файлы App.css, App.test.js, index.css, logo.svg, setupTests.js и reportWebVitals.js. Мы также можем изменить код index.js, чтобы вывести его из безопасного режима, что не позволит нашему веб-сайту выполнять дополнительные вызовы API.

index.js должен выглядеть так:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

Наконец, мы можем удалить все из файла App.js, чтобы он выглядел так:

import React from "react"
import axios from "axios"

const App = () => {
  return (
    <div> Hello World </div>
  )
}

export default App

Если ваш проект работает правильно, вы должны увидеть надпись Hello World в своем браузере на https://localhost:3000. Большой! Наконец-то мы можем начать.

Первое, что мы хотим сделать, это настроить базовый экран входа в систему, который поможет нам получить доступ к данным пользователя. В API Spotify есть много документации по различным типам потоков аутентификации, которые вы потенциально можете использовать. Различные потоки имеют разные уровни безопасности и сложности. Ради этого проекта мы будем использовать Implicit Grant Flow, который является самым простым в настройке из четырех. Однако рефакторинг вашего кода для работы с кодом авторизации с PKCE, который является наиболее безопасным методом, не так уж сложен. Если вы хотите узнать об этом больше, вы можете прочитать официальную документацию Spotify здесь.

Общая идея неявного потока грантов заключается в том, что как только пользователь войдет в систему через портал Spotify, Spotify отправит пользователя обратно на наш сайт с дополнительной информацией (называемой токеном) в URI. Затем мы можем проанализировать этот новый URI и сохранить токен, чтобы использовать его для любых вызовов API, которые нам нужны.

Мы начнем с создания простого компонента экрана входа в систему (который будет простой кнопкой с надписью «Войти в Spotify»). Мы сделаем это, создав файл Login.js и введя следующий код:

import React from 'react'

const LoginButton = () => {
  return (
    <a> Login to Spotify </a>
  )
}

export default LoginButton

Чтобы отобразить эту кнопку входа на нашем веб-сайте, нам нужно изменить App.js и его операторы импорта. Мы также импортируем хуки useState и useEffect, которые будем использовать позже. Вместе это должно выглядеть так:

import React, { useState, useEffect } from "react"
import LoginButton from "./Login"

const App = () => {
  return (
    <LoginButton />
  )
}

export default App

Далее мы добавим функциональность нашей кнопке входа. Если вам нужна дополнительная информация о том, как работает неявный поток грантов, вы можете посмотреть документацию Spotify здесь. Чтобы добавить функциональность для кнопки, нам нужно добавить соответствующий тег href к нашей кнопке, чтобы она ссылалась на правильную конечную точку. Мы можем сделать это, определив следующие константы.

const CLIENT_ID = "YOUR CLIENT ID FROM SPOTIFY API WEBSITE"
const REDIRECT_URI = "https://localhost:3000"
const AUTH_ENDPOINT = "https://accounts.spotify.com/authorize"
const RESPONSE_TYPE = "token"
const SCOPES = ['user-top-read']
const loginEndpoint = `${AUTH_ENDPOINT}?client_id=${CLIENT_ID}&scope=${SCOPES}&redirect_uri=${REDIRECT_URI}&response_type=${RESPONSE_TYPE}&show_dialog=true`

Мы добавляем область «пользователь-самое-самое-читаемое», потому что нам нужно разрешение от пользователя, чтобы просмотреть его историю прослушивания (включая его лучших исполнителей и треки). Мы можем связать это с нашей кнопкой входа, изменив наш тег ‹a› в Login.js следующим образом:

<a href={loginEndpoint}> Login to Spotify </a>

Теперь, если вы нажмете кнопку на нашем веб-сайте, он должен перенаправить вас на портал аутентификации Spotify, а затем перенаправить вас обратно на веб-сайт. Вы также должны увидеть измененный URI в своем браузере, который содержит специальный токен доступа.

Далее нам нужно декодировать этот токен из URI и использовать его для вызовов нашего API. Мы можем расшифровать токен доступа, добавив следующий код в App.js:

// ...

const App = () => {

  const [token, setToken] = useState('')

  const getTokenFromURI = () => {
    const oldToken = window.localStorage.getItem("token")
    if (oldToken) { return oldToken }

    const hash = window.location.hash
    if (!hash) { return '' }

    const newToken = hash.substring(1).split("&").find(elem => elem.startsWith("access_token")).split("=")[1]
    return newToken
  }

  useEffect(() => { 
    setToken(getTokenFromURI())
    window.location.hash = ""
    window.localStorage.setItem("token", token)
  }, [])

  // ...

}

export default App

Короче говоря, мы смотрим на URI, определяя хеш-константу как подстроку URI после #. Если у нас есть # в URI и у нас еще нет предыдущего токена, мы декодируем хэш и обновляем наш токен переменной состояния. Поместив эту функцию в хук useEffect без каких-либо зависимостей, мы гарантируем, что она запускается только один раз при открытии страницы, а не в каждом кадре. Если вы хотите узнать больше о хуках useState и useEffect, вы можете посмотреть официальную документацию React здесь.

Поскольку у нас есть переменная состояния для пользовательского токена Spotify, у нас фактически есть способ сказать, что пользователь правильно аутентифицирован. Используя это, мы можем создать два отдельных представления для пользователя в зависимости от того, вошли ли они в систему. Чтобы реализовать это, мы изменим код App.js, включив в него следующее:

// ...
import Navbar from "./Navbar"

const App = () => {

  // ...

  if (token === '') {
    return ( 
      // Can replace with more complex login screen component
      <LoginButton />
     )
  }

  return (
    <div>
      <Navbar />
      <MusicList />
    </div>
  )
}

export default App

Нам также нужно сделать компонент Navbar.js соответствующим образом. Ради этого поста мы сделаем очень простой компонент Navbar с минимальным количеством информации на экране. Если вы хотите узнать больше о стилях CSS и о том, как внедрить передовые методы веб-разработки, вы можете прочитать больше в статье freeCodeCamp о стилях приложений React здесь.

Если вы хотите увидеть, как могла бы выглядеть полностью разработанная версия этого проекта, перейдите здесь.

Прежде чем мы создадим наш компонент Navbar, нам нужно объявить следующие переменные состояния и передать их другим нашим компонентам в качестве свойств в нашем файле App.js:

// Imports

const App = () => {

  // ...
 
  const [trackList, setTrackList] = useState([])
  const [artistList, setArtistList] = useState([])
  const [recList, setRecList] = useState([])
  const [searchType, setSearchType] = useState('tracks')
  const [searchLength, setSearchLength] = useState('short_term')

  // ...

  return (
    <div>
      <Navbar 
        setSearchType = {setSearchType} 
        setSearchLength = {setSearchLength} 
        setToken = {setToken}
       />
    </div>
  )

}

export default App

Наш базовый компонент Navbar.js будет включать следующие функции:

  1. Выпадающий список для выбора между треками, исполнителями и рекомендациями, которые будут 3 основными функциями веб-сайта.
  2. Раскрывающийся список для выбора одного из краткосрочных, среднесрочных и долгосрочных периодов, которые будут определять временной диапазон, на который будет смотреть Spotify.
  3. Кнопка выхода

Мы можем инициализировать эти 3 функции и настроить их функциональность с помощью наших ранее объявленных переменных состояния, используя следующий код:

import React from 'react'

const Navbar = (props) => {
  return (
    <div>
        <select onChange={e => props.setSearchType(e.target.value)}>
            <option value="tracks"> Tracks </option>
            <option value="artists"> Artists </option>
            <option value="recommendations"> Recommendations </option>
         </select>

        <select onChange={e => props.setSearchLength(e.target.value)}>
            <option value="short_term"> Short Term </option>
            <option value="medium_term"> Medium Term </option>
            <option value="long_term"> Long Term </option>
         </select>

         <button> Log Out </button>

    </div>
  )
}

export default Navbar

Прежде чем мы сможем отобразить какие-либо данные пользователя на экране, нам нужно иметь возможность обрабатывать вызовы API для Spotify и OpenAI. Для этого мы собираемся настроить сервер Node.js Express, чтобы мы могли безопасно выполнять эти вызовы.

Если вы никогда раньше не работали с Node.js или не понимаете, что означает настройка веб-сервера, Fireship предлагает чрезвычайно простое и информативное видео, объясняющее, как настроить ваш первый сервер шаг за шагом. -шаг, который вы можете проверить здесь.

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

npm init -y
npm i axios cors dotenv express openai

На этом этапе вам нужно будет получить свой личный ключ API от OpenAI. Вы можете сделать это здесь. Вам нужно будет включить выставление счетов в своей учетной записи OpenAI, и с вас будет взиматься плата за вызовы API, которые вы делаете. Однако стоимость вызова gpt-3.5-turbo, модели, которую мы будем использовать, составляет доли цента за вызов API, поэтому вам не нужно беспокоиться о том, что вам выставят счет на значительную сумму.

Получив этот ключ API, вы можете скопировать и вставить файл .env в папку вашего сервера. Это позволяет вашему файлу server.js получать доступ к информации, но не будет отображать информацию публично, если вы решите развернуть свой веб-сайт.

Ваш файл .env должен выглядеть следующим образом:

OPENAI_API_KEY = "YOUR-OPENAI-API-KEY-HERE"

Наш файл server.js будет выглядеть следующим образом:

require("dotenv").config();
const cors = require('cors');
const { Configuration, OpenAIApi } = require("openai");
const axios = require("axios");
const express = require("express");
const app = express();
app.use(cors());

app.use(express.json());

const port = process.env.PORT || 4000;


const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY });
const openai = new OpenAIApi(configuration);


// ASK-OPEN-AI -- MAKE REQUEST TO CHATGPT API
// PARAMETERS:
// - prompt (required): Prompt which will be sent to GPT3.5-Turbo
app.get('/ask-open-ai', async (req, res) => {
    const prompt = req.query.prompt;
  
    try {
      if (prompt == null) {
        throw new Error("No prompt provided.");
      }

      const response = await openai.createChatCompletion({
        model: "gpt-3.5-turbo",
        messages: [
          {
            role: "assistant",
            content: prompt
          },
        ],
      });

      const completion = response.data.choices[0].message.content;

      return res.status(200).json({
        success: true,
        message: completion,
      });

    } catch (error) {
      return res.status(400).json({
        success: false,
        message: error.message
        });
    }
});

// GET-TOP-SPOTIFY -- GET USER MOST LISTENED TO DATA
// PARAMETERS:
// - search_type (required): Search type used in API call (must be 'tracks' or 'artists)
// - access_token (required): User Spotify access token to allow us to look at their account data
// - time_range (required): Length of search used in Spotify API call(must be 'short_term', 'medium_term', or 'long_term')
// - offset (optional): Index from which to start getting top 50 data (we will use offset 49 to get user's 51-99) (Default Value: 0)
app.get('/get-top-spotify', async (req, res) => {
    const search_type = req.query.search_type;
    const access_token = req.query.access_token;
    const time_range = req.query.time_range;
    const offset = req.query.offset;

    try {
        if (access_token == null) {
            throw new Error("No access token provided.");
        }

        if (search_type !== 'tracks' && search_type !== 'artists') {
            throw new Error("Invalid search query provided.");
        }

        if (time_range !== 'short_term' && time_range !== 'medium_term' && time_range !== 'long_term') {
            throw new Error("Invalid time range provided.");
        }

        if (offset == null) {
            offset = 0;
        }

        const response = await axios.get(`https://api.spotify.com/v1/me/top/${search_type}?`, {
            headers: {
                Authorization: `Bearer ${access_token}`
            },
            params: {
                limit: 50,
                offset: offset,
                time_range: time_range
              }
        });

        completion = response.data.items
        return res.status(200).json({
            success: true,
            message: completion,
        });

    } catch (error) {
        console.log(error.message)
        return res.status(400).json({
            success: false,
            message: error.message
        })
    }
})

// SEARCH-SPOTIFY -- GET MOST RELEVANT TRACK OBJECT FROM SPOTIFY BASED ON SEARCH QUERY
// PARAMETERS:
// - search_query (required): Query used to find track (Similar to searching on Spotify App)
// - access_token (required): User Spotify access token to allow us to make Spotify searches
app.get('/search-spotify', async (req, res) => {
    const search_query = req.query.search_query;
    const access_token = req.query.access_token;

    try {
        if (access_token == null) {
            throw new Error("No access token provided.");
        }

        if (search_query == null) {
            throw new Error("No search query provided.");
        }

        const response = await axios.get(`https://api.spotify.com/v1/search?q=${search_query}&type=track&limit=1`, {
            headers: { Authorization: `Bearer ${access_token}` },
        });

        completion = response.data.tracks.items[0]
        return res.status(200).json({
            success: true,
            message: completion,
        });
        
    } catch (error) {
        return res.status(400).json({
            success: false,
            message: error.message
        })
    }
})

app.listen(port, () => console.log(`Server is running on port ${port}`));

Подводя итог, мы объявили веб-сервер, к которому мы можем получить локальный доступ по адресу https://localhost:4000. Не вдаваясь в подробности того, как работают серверы Express, мы создали следующие 3 конечные точки:

  1. ask-open-ai — позволяет передать поисковый запрос и получить ответ от GPT-3.5-Turbo
  2. get-top-spotify — позволяет нам получить доступ к 99 лучшим трекам или исполнителям пользователя в краткосрочной, среднесрочной и долгосрочной перспективе.
  3. search-spotify — позволяет передать поисковый запрос и возвращает наиболее релевантную песню на основе нашего поиска.

Наш веб-сервер уже имеет необходимую информацию для аутентификации, хранящуюся в его файле .env, что позволяет нам безопасно выполнять эти вызовы. Хотя мы единственные, кто будет использовать эти конечные точки, рекомендуется обрабатывать все крайние случаи, например, если кто-то делает вызов API к нашему серверу без ввода поискового запроса или не предоставляет свой токен доступа. В этих случаях мы выводим результат с сообщениями об ошибках и сообщаем об ошибке, чтобы упростить отладку.

Если вы никогда раньше не работали с серверами Express.js, я рекомендую просмотреть приведенный выше код и попытаться понять каждую строку кода и его назначение. Ради этого поста в блоге мы продолжим.

Чтобы запустить сервер, выполните следующую команду в терминале после перехода в папку сервера.

node server.js

Если все сделано правильно, вы должны увидеть сообщение о том, что «Сервер работает на порту 4000».

Мы можем проверить, работают ли наши конечные точки, выполнив HTTP-запрос в нашем браузере. Скопируйте и вставьте следующую ссылку в свой браузер и убедитесь, что вы видите соответствующий ответ от GPT3.5:

https://localhost:4000/ask-open-ai?prompt='What is the most listened to track on Spotify of all time'

Ответ должен выглядеть примерно так:

Мы не можем так же легко протестировать конечные точки Spotify, поскольку для их использования нам нужен токен доступа от Spotify, что будет проще сделать в контексте нашего фактического приложения, поэтому мы продолжим.

Теперь, когда наш сервер полностью настроен, мы можем вернуться к нашему внешнему интерфейсу и настроить код, который выполняет эти вызовы API и сохраняет важную информацию из данных, которые мы получаем обратно.

В App.js мы добавляем следующий вызов асинхронной функции:

const GetUserInfo = async (searchType, offset) => {
    const response = await axios.get(`${PORT}/get-top-spotify`, {
      params: {
        search_type: searchType,
        access_token: token,
        time_range: searchLength,
        offset: offset
      }
    })

    return response.data.message;
  }

Эта функция принимает searchType (треки или исполнители) и значение смещения и возвращает список из 50 объектов Track или Artist с соответствующей информацией.

Затем нам нужна следующая функция, которая будет декодировать информацию, возвращаемую этой функцией.

const UnwrapSpotifyData = (items, itemType) => {
    try {
      if (itemType === 'artists') {
        return items.map(artist => (
          { name: artist.name, picture: artist.images[0].url, link: artist.external_urls.spotify }
        ))
      }

      return items.map(track => (
        { name: track.name, artist: track.artists[0].name, picture: track.album.images[0].url, explicit: track.explicit, duration: track.duration_ms, link: track.external_urls.spotify }
      ))
    } catch (error) {
      console.log(error.message)
    }
  }

Он берет каждый объект, возвращенный вызовом API, и сохраняет имя, URL-адрес изображения, ссылку и другие подобные функции в объекте JavaScript, к которому мы можем получить доступ в нашем компоненте MusicList.js.

Наконец, мы хотим вызывать эти функции как для исполнителей, так и для треков каждый раз, когда пользователь меняет длину своего поиска. И мы хотим, чтобы эти функции изменяли наши переменные состояния, которые мы уже объявили. Мы можем реализовать это, используя следующий хук useEffect.

useEffect(() => {
    const fetchData = async () => {
      try {
        const first50TracksResponse = await GetUserInfo('tracks', 0)
        const last50TracksResponse = await GetUserInfo('tracks', 49)
        const first50ArtistsResponse = await GetUserInfo('artists', 0)
        const last50ArtistsResponse = await GetUserInfo('artists', 49)

        const first50Tracks = UnwrapSpotifyData(first50TracksResponse, 'tracks')
        const last50Tracks = UnwrapSpotifyData(last50TracksResponse, 'tracks').slice(1)
        const first50Artists = UnwrapSpotifyData(first50ArtistsResponse, 'artists')
        const last50Artists = UnwrapSpotifyData(last50ArtistsResponse, 'artists').slice(1)

        setTrackList([...first50Tracks, ...last50Tracks]);
        setArtistList([...first50Artists, ...last50Artists]);

      } catch (error) {
        console.error('Error fetching data:', error);
      }
    }
  
    fetchData();
  }, [searchLength]);

(Примечание: мы отсекаем первый элемент в каждом из массивов last50Artists/Tracks, чтобы не повторять трек/исполнителя, занимающие 50-е место дважды в нашем списке)

Это вызывает обе только что созданные функции 4 раза и сохраняет объекты JS в наших переменных состояния trackList и artistList. Поскольку мы помещаем searchLength в массив зависимостей, эти функции будут вызываться каждый раз, когда пользователь изменяет длину поиска в раскрывающемся списке.

Последний шаг к созданию работающего веб-сайта — это, наконец, создание нашего компонента MusicList.js. Мы передадим ему следующую поддержку в App.js:

const App = () => {

  // ...

  return (
    <div>
      <Navbar 
        setSearchType = {setSearchType} 
        setSearchLength = {setSearchLength} 
        setToken = {setToken}
      />

      <MusicList 
        listInfo = {searchType == 'tracks' ? trackList : artistList}
      />
    </div> 
  )

}

export default App

Этот тернарный оператор дает ему соответствующий список на основе нашей переменной состояния searchType. Наш простой файл MusicList.js будет отображать только имя исполнителя/трека, имя исполнителя (если элемент является треком) и рейтинг каждого элемента. Однако с помощью информации, которую мы можем получить из API, мы потенциально можем отобразить явный символ для откровенных песен, обложку альбома, ссылку на песню в Spotify, а также различные другие показатели Spotify, такие как популярность песни. Spotify API предлагает множество возможностей.

Наш упрощенный файл MusicList.js выглядит так:

import React from 'react'

const MusicList = (props) => {
  return (
    props.listInfo.map((item, index) =>
      <div> {index + 1}. {item.name} {item.artist ? ` --- ${item.artist}` : ''}</div>
    )
  )
}

export default MusicList

Наконец, каждый раз, когда мы работаем с асинхронными вызовами API, хорошей практикой является предоставление пользователю какой-либо обратной связи, чтобы знать, что веб-сайт отвечает. Это особенно относится к нашим вызовам API к OpenAI, которые иногда могут занимать до 30 секунд. Для этого мы создадим переменную состояния, которая отображает счетчик, если установлено значение true. Это покажет пользователю, что что-то происходит, когда он нажимает различные раскрывающиеся списки и кнопки.

Мы реализуем это, выполнив следующие действия:

  1. Создайте файл Navbar.css и добавьте следующий код:
.spinner {
    display: grid;
    width: 15px;
    height: 15px;
    border: 2px solid #33CC33;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
}

2. Создайте переменную состояния isLoading в App.js и передайте ее в качестве реквизита в наш компонент Navbar:

// Imports

const App = () => {
  
  // ...
  
  const [isLoading, setIsLoading] = useState(false)

  // ...

  return (
    <div>
      <Navbar 
        setSearchType = {setSearchType} 
        setSearchLength = {setSearchLength} 
        setToken = {setToken}
        isLoading = {isLoading}
      />
     
     // ...

    </div>
  )

}

export default App

3. В Navbar.js импортируйте файл CSS и условно отобразите счетчик:

// Imports
import './Navbar.css'

const Navbar = (props) => {
  return (
    <div>

      // ...

      { props.isLoading && <div className="spinner"></div> }

    </div>
  )
}

export default Navbar

4. В App.js обновляйте переменную состояния isLoading всякий раз, когда мы делаем вызовы API:

useEffect(() => {
    const fetchData = async () => {
      try {
        setIsLoading(true)

        const first50TracksResponse = await GetUserInfo('tracks', 0)

        // ...        

        setArtistList([...first50Artists, ...last50Artists]);

        setIsLoading(false)

      } catch (error) {
        console.error('Error fetching data:', error);
        setIsLoading(false)
      }
   }
  
   fetchData();
}, [searchLength]);

На этом мы полностью завершили работу над частью веб-сайта Spotify. Теперь мы можем успешно получить доступ к лучшим трекам и исполнителям пользователя для всех 3 длин поиска. Теперь мы можем начать связывать эту информацию с GPT3.5-Turbo и начинать давать рекомендации.

Хотя OpenAI утверждает, что не хранит какие-либо данные, которые собирает через вызовы API, важно не передавать информацию о пользователе другим сторонним источникам без предварительного запроса разрешения. Сделаем это в виде кнопки, когда пользователь открывает вкладку с рекомендациями. По умолчанию мы не будем отправлять какие-либо данные пользователя в OpenAI. Пользователю придется вручную нажимать кнопку (то есть давать разрешение) каждый раз, когда он хочет получить больше рекомендаций от GPT3.5-Turbo.

Начнем с создания нового компонента RecList.js, инициализированного следующим образом:

import React from 'react'
import axios from 'axios'

const RecList = (props) => {
  return (
    <div>RecList</div>
  )
}

export default RecList

Мы также сделаем необходимые импорты и условные операторы для отображения этого компонента, если для нашей переменной состояния searchType установлено значение «рекомендации». Мы также передадим необходимый реквизит.

// ...

import RecList from './RecList'

const App = () => {
  
  // ...
  
  return (
    <div>
      <Navbar 
        setSearchType = {setSearchType} 
        setSearchLength = {setSearchLength} 
        setToken = {setToken}
      />

      { (searchType === 'recommendations') && 
        <RecList 
          recList = {recList}
          setRecList = {setRecList}
          trackList = {trackList}
        />
      }

      { (searchType !== 'recommendations') &&
        <MusicList 
          listInfo = {searchType == 'tracks' ? trackList : artistList}
        />
      }

      
    </div>
  )

}

export default App