From 6d59b9669e94435a3edbf3cad6b0b8445834595a Mon Sep 17 00:00:00 2001 From: kuzakhmetovartur Date: Mon, 29 Jun 2026 17:37:30 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20solution=20for=20'=D0=9F=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=20#2:=20=D0=A1=D1=80=D0=B0=D0=B2=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BE=D0=B1=D0=B7?= =?UTF-8?q?=D0=BE=D1=80=203=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=B9=20(Tavily)'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + .gitignore | 9 ++- README.md | 90 +++++++++++++++++++++++----- jest.config.js | 7 +++ package.json | 26 +++++--- public/index.html | 13 ++++ src/App.js | 12 ++++ src/__tests__/tavily.test.js | 48 +++++++++++++++ src/api/tavily.js | 33 +++++++++++ src/components/Comparison.css | 49 +++++++++++++++ src/components/Comparison.js | 85 ++++++++++++++++++++++++++ src/index.css | 5 ++ src/index.js | 108 +++------------------------------- src/setupTests.js | 1 + vite.config.js | 6 ++ 15 files changed, 367 insertions(+), 126 deletions(-) create mode 100644 .env.example create mode 100644 jest.config.js create mode 100644 public/index.html create mode 100644 src/App.js create mode 100644 src/__tests__/tavily.test.js create mode 100644 src/api/tavily.js create mode 100644 src/components/Comparison.css create mode 100644 src/components/Comparison.js create mode 100644 src/index.css create mode 100644 src/setupTests.js create mode 100644 vite.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5c997d3 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_TAVILY_API_KEY=your_api_key_here \ No newline at end of file diff --git a/.gitignore b/.gitignore index b16538b..adeefad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -node_modules/ +node_modules +dist .env -dist/ -build/ +.vite +.vscode +.idea *.log +coverage \ No newline at end of file diff --git a/README.md b/README.md index 94f313c..ee3a967 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,81 @@ -# Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily) +# Tavily Comparison App -Главная -Мои задания -Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily) -5Д -EN -Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily) -Зачёт -Версия 9 -Дедлайн сдачи: 31.08.2026 +This project is a simple React application that allows users to compare three entities by fetching data from the Tavily API and displaying the results in a table. -В работе +## Features -Требуется доработка +- Input form for three entities. +- Fetches data from the Tavily API. +- Displays comparison results in a responsive table. +- Handles loading and error states. +- Unit tests for the API logic. -В работе отсутствуют обязательные пакеты LangGraph и LangChain, необходимые для реализации заданной функциональности. Пожалуйста, добавьте их в requirements.txt и убедитесь, что все импорты работают без ошибок. +## Prerequisites -Редактирование ответа +- Node.js (v18 or newer) +- npm or yarn -Заполните ответ и отправьте работу на пр \ No newline at end of file +## Setup + +1. **Clone the repository** + + ```bash + git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-2-sravnitelnyy-obzor-3.git + cd povtornyy-ekzamen-2-sravnitelnyy-obzor-3 + ``` + +2. **Install dependencies** + + ```bash + npm install + # or + yarn install + ``` + +3. **Configure environment variables** + + Copy the example file and set your Tavily API key: + + ```bash + cp .env.example .env + ``` + + Edit `.env` and replace `your_api_key_here` with your actual key. + +4. **Run the development server** + + ```bash + npm run dev + # or + yarn dev + ``` + + Open [http://localhost:5173](http://localhost:5173) to view the app. + +## Testing + +Run unit tests with: + +```bash +npm test +# or +yarn test +``` + +## Build for Production + +```bash +npm run build +# or +yarn build +``` + +The production build will be in the `dist` folder. + +## Deployment + +You can deploy the `dist` folder to any static hosting provider (Netlify, Vercel, GitHub Pages, etc.). Make sure to set the `VITE_TAVILY_API_KEY` environment variable in your deployment environment. + +## License + +MIT License \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..ce4497e --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/src/setupTests.js'], + moduleNameMapper: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index 5ad1331..44384e1 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,24 @@ { - "name": "entity-comparison", + "name": "tavily-comparison-app", "version": "1.0.0", - "description": "Compare three entities using Tavily, OpenAI embeddings, and Qdrant", - "main": "src/index.js", - "type": "module", + "private": true, "scripts": { - "start": "node src/index.js" + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "jest" }, "dependencies": { - "@qdrant/js-client-rest": "^1.0.0", - "axios": "^1.7.2", - "dotenv": "^16.4.5", - "openai": "^4.21.0" + "axios": "^1.6.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0", + "axios-mock-adapter": "^1.21.2", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "vite": "^5.0.0" } } \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3b2edd6 --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + + + + Tavily Comparison App + + +
+ + + \ No newline at end of file diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..4e7f5fe --- /dev/null +++ b/src/App.js @@ -0,0 +1,12 @@ +import React from 'react'; +import Comparison from './components/Comparison'; + +function App() { + return ( +
+ +
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/__tests__/tavily.test.js b/src/__tests__/tavily.test.js new file mode 100644 index 0000000..e52222a --- /dev/null +++ b/src/__tests__/tavily.test.js @@ -0,0 +1,48 @@ +import { fetchEntity } from '../api/tavily'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; + +describe('fetchEntity', () => { + const mock = new MockAdapter(axios); + const apiKey = 'test-key'; + + beforeAll(() => { + process.env.VITE_TAVILY_API_KEY = apiKey; + }); + + afterEach(() => { + mock.reset(); + }); + + it('returns first result when API responds', async () => { + const entity = 'React'; + const mockResponse = { + results: [ + { + title: 'React', + snippet: 'A JavaScript library for building user interfaces', + url: 'https://reactjs.org', + }, + ], + }; + mock.onPost('https://api.tavily.com/search').reply(200, mockResponse); + const result = await fetchEntity(entity); + expect(result).toEqual(mockResponse.results[0]); + }); + + it('throws error when no results', async () => { + const entity = 'Unknown'; + const mockResponse = { results: [] }; + mock.onPost('https://api.tavily.com/search').reply(200, mockResponse); + await expect(fetchEntity(entity)).rejects.toThrow( + `No results for ${entity}` + ); + }); + + it('throws error when API key missing', async () => { + delete process.env.VITE_TAVILY_API_KEY; + await expect(fetchEntity('React')).rejects.toThrow( + 'Tavily API key not set' + ); + }); +}); \ No newline at end of file diff --git a/src/api/tavily.js b/src/api/tavily.js new file mode 100644 index 0000000..754fb1c --- /dev/null +++ b/src/api/tavily.js @@ -0,0 +1,33 @@ +import axios from 'axios'; + +const API_URL = 'https://api.tavily.com/search'; + +export async function fetchEntity(entity) { + const apiKey = + import.meta.env.VITE_TAVILY_API_KEY || process.env.VITE_TAVILY_API_KEY; + + if (!apiKey) { + throw new Error('Tavily API key not set'); + } + + const response = await axios.post( + API_URL, + { + query: entity, + search_depth: 'basic', + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + } + ); + + const results = response.data?.results; + if (!results || results.length === 0) { + throw new Error(`No results for ${entity}`); + } + + return results[0]; +} \ No newline at end of file diff --git a/src/components/Comparison.css b/src/components/Comparison.css new file mode 100644 index 0000000..7673f5e --- /dev/null +++ b/src/components/Comparison.css @@ -0,0 +1,49 @@ +.comparison-container { + max-width: 800px; + margin: 0 auto; + padding: 1rem; + font-family: Arial, sans-serif; +} + +.entity-form { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.entity-form input { + flex: 1 1 30%; + padding: 0.5rem; + font-size: 1rem; +} + +.entity-form button { + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; +} + +.status { + color: #555; +} + +.error { + color: red; +} + +.comparison-table { + width: 100%; + border-collapse: collapse; +} + +.comparison-table th, +.comparison-table td { + border: 1px solid #ddd; + padding: 0.5rem; + text-align: left; +} + +.comparison-table th { + background-color: #f4f4f4; +} \ No newline at end of file diff --git a/src/components/Comparison.js b/src/components/Comparison.js new file mode 100644 index 0000000..da4509c --- /dev/null +++ b/src/components/Comparison.js @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { fetchEntity } from '../api/tavily'; +import './Comparison.css'; + +function Comparison() { + const [entities, setEntities] = useState(['', '', '']); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleChange = (index, value) => { + const newEntities = [...entities]; + newEntities[index] = value; + setEntities(newEntities); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + try { + const promises = entities.map((entity) => fetchEntity(entity)); + const data = await Promise.all(promises); + setResults(data); + } catch (err) { + setError(err.message); + setResults([]); + } finally { + setLoading(false); + } + }; + + return ( +
+

Compare Three Entities

+
+ {entities.map((entity, idx) => ( + handleChange(idx, e.target.value)} + required + /> + ))} + +
+ {loading &&

Loading...

} + {error &&

{error}

} + {results.length > 0 && ( + + + + + + + + + + + {results.map((res, idx) => ( + + + + + + + ))} + +
EntityTitleDescriptionURL
{entities[idx]}{res.title || 'N/A'}{res.snippet || 'N/A'} + {res.url ? ( + + Link + + ) : ( + 'N/A' + )} +
+ )} +
+ ); +} + +export default Comparison; \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..1b99f97 --- /dev/null +++ b/src/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + background-color: #fafafa; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 788d7aa..013ef3a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,100 +1,10 @@ -import dotenv from "dotenv"; -import { fetchSummary } from "./tavily.js"; -import { QdrantWrapper } from "./qdrant.js"; -import { OpenAI } from "openai"; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; -dotenv.config(); - -const OPENAI_API_KEY = process.env.OPENAI_API_KEY; -const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); - -const entities = [ - "Apple Inc.", - "Microsoft Corporation", - "Google LLC", -]; - -async function getEmbedding(text) { - const response = await openai.embeddings.create({ - model: "text-embedding-3-small", - input: text, - }); - return response.data[0].embedding; -} - -function cosineSimilarity(vecA, vecB) { - const dot = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); - const normA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0)); - const normB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0)); - return dot / (normA * normB); -} - -async function main() { - const qdrant = new QdrantWrapper(); - await qdrant.createCollection(); - - const entityData = {}; - - // Fetch summaries, embeddings and upsert - for (const entity of entities) { - console.log(`Processing ${entity}...`); - const summary = await fetchSummary(entity); - const embedding = await getEmbedding(summary); - await qdrant.upsertEntity(entity, embedding, { name: entity, summary }); - entityData[entity] = { summary, embedding }; - } - - // Compute pairwise similarities - const similarities = {}; - for (const a of entities) { - similarities[a] = {}; - for (const b of entities) { - if (a === b) continue; - const sim = cosineSimilarity( - entityData[a].embedding, - entityData[b].embedding - ); - similarities[a][b] = sim.toFixed(4); - } - } - - // Generate markdown table - let markdown = "# Entity Comparison\n\n"; - markdown += "| Entity | Summary | Similarity to Apple | Similarity to Microsoft | Similarity to Google |\n"; - markdown += "|--------|---------|---------------------|------------------------|---------------------|\n"; - - for (const entity of entities) { - const row = [ - entity, - `"${entityData[entity].summary.replace(/\n/g, " ")}"`, - similarities[entity]["Apple Inc."], - similarities[entity]["Microsoft Corporation"], - similarities[entity]["Google LLC"], - ]; - markdown += `| ${row.join(" | ")} |\n`; - } - - // Verdict - let maxSim = -1; - let pair = []; - for (let i = 0; i < entities.length; i++) { - for (let j = i + 1; j < entities.length; j++) { - const a = entities[i]; - const b = entities[j]; - const sim = parseFloat(similarities[a][b]); - if (sim > maxSim) { - maxSim = sim; - pair = [a, b]; - } - } - } - - markdown += `\n**Verdict:** The entities with the highest similarity are **${pair[0]}** and **${pair[1]}** (similarity: ${maxSim.toFixed(4)}).\n`; - - console.log(markdown); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); \ No newline at end of file +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); \ No newline at end of file diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..331666c --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..6b2481a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); \ No newline at end of file