mirror of
https://git.mirrors.martin98.com/https://github.com/mendableai/firecrawl
synced 2025-08-04 11:50:39 +08:00
Firecrawl UI Template
Firecrawl UI template
This commit is contained in:
commit
a4bccbe3bb
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,4 +17,5 @@ apps/test-suite/logs
|
||||
apps/test-suite/load-test-results/test-run-report.json
|
||||
|
||||
apps/playwright-service-ts/node_modules/
|
||||
apps/playwright-service-ts/package-lock.json
|
||||
apps/playwright-service-ts/package-lock.json
|
||||
|
||||
|
@ -405,12 +405,12 @@ _It is the sole responsibility of the end users to respect websites' policies wh
|
||||
|
||||
## License Disclaimer
|
||||
|
||||
This project is primarily licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), as specified in the LICENSE file in the root directory of this repository. However, certain components of this project, specifically the SDKs located in the `/apps/js-sdk` and `/apps/python-sdk` directories, are licensed under the MIT License.
|
||||
This project is primarily licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), as specified in the LICENSE file in the root directory of this repository. However, certain components of this project are licensed under the MIT License. Refer to the LICENSE files in these specific directories for details.
|
||||
|
||||
Please note:
|
||||
|
||||
- The AGPL-3.0 license applies to all parts of the project unless otherwise specified.
|
||||
- The SDKs in `/apps/js-sdk` and `/apps/python-sdk` are licensed under the MIT License. Refer to the LICENSE files in these specific directories for details.
|
||||
- The SDKs and some UI components are licensed under the MIT License. Refer to the LICENSE files in these specific directories for details.
|
||||
- When using or contributing to this project, ensure you comply with the appropriate license terms for the specific component you are working with.
|
||||
|
||||
For more details on the licensing of specific components, please refer to the LICENSE files in the respective directories or contact the project maintainers.
|
||||
|
18
apps/ui/ingestion-ui/.eslintrc.cjs
Normal file
18
apps/ui/ingestion-ui/.eslintrc.cjs
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
24
apps/ui/ingestion-ui/.gitignore
vendored
Normal file
24
apps/ui/ingestion-ui/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
21
apps/ui/ingestion-ui/LICENSE
Normal file
21
apps/ui/ingestion-ui/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Sideguide Technologies Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
65
apps/ui/ingestion-ui/README.md
Normal file
65
apps/ui/ingestion-ui/README.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Firecrawl UI Template
|
||||
|
||||
This template provides an easy way to spin up a UI for Firecrawl using React. It includes a pre-built component that interacts with the Firecrawl API, allowing you to quickly set up a web crawling and scraping interface.
|
||||
|
||||
## ⚠️ Important Security Notice
|
||||
|
||||
**This template exposes Firecrawl API keys in the client-side code. For production use, it is strongly recommended to move API interactions to a server-side implementation to protect your API keys.**
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v14 or later recommended)
|
||||
- npm
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Set up your Firecrawl API key:
|
||||
Open `src/components/FirecrawlComponent.tsx` and replace the placeholder API key:
|
||||
|
||||
```typescript
|
||||
const FIRECRAWL_API_KEY = "your-api-key-here";
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. Open your browser and navigate to the port specified in your terminal
|
||||
|
||||
## Customization
|
||||
|
||||
The main Firecrawl component is located in `src/components/FirecrawlComponent.tsx`. You can modify this file to customize the UI or add additional features.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
For production use, consider the following security measures:
|
||||
|
||||
1. Move API interactions to a server-side implementation to protect your Firecrawl API key.
|
||||
2. Implement proper authentication and authorization for your application.
|
||||
3. Set up CORS policies to restrict access to your API endpoints.
|
||||
|
||||
## Learn More
|
||||
|
||||
For more information about Firecrawl and its API, visit the [Firecrawl documentation](https://docs.firecrawl.dev/).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
The Firecrawl Ingestion UI Template is licensed under the MIT License. This means you are free to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the SDK, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Please note that while this SDK is MIT licensed, it is part of a larger project which may be under different licensing terms. Always refer to the license information in the root directory of the main project for overall licensing details.
|
17
apps/ui/ingestion-ui/components.json
Normal file
17
apps/ui/ingestion-ui/components.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
16
apps/ui/ingestion-ui/index.html
Normal file
16
apps/ui/ingestion-ui/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Firecrawl UI Template</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
4647
apps/ui/ingestion-ui/package-lock.json
generated
Normal file
4647
apps/ui/ingestion-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
apps/ui/ingestion-ui/package.json
Normal file
42
apps/ui/ingestion-ui/package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "ingestion-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.414.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.3.4"
|
||||
}
|
||||
}
|
6
apps/ui/ingestion-ui/postcss.config.js
Normal file
6
apps/ui/ingestion-ui/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
apps/ui/ingestion-ui/public/favicon.ico
Normal file
BIN
apps/ui/ingestion-ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1
apps/ui/ingestion-ui/public/vite.svg
Normal file
1
apps/ui/ingestion-ui/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
42
apps/ui/ingestion-ui/src/App.css
Normal file
42
apps/ui/ingestion-ui/src/App.css
Normal file
@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
12
apps/ui/ingestion-ui/src/App.tsx
Normal file
12
apps/ui/ingestion-ui/src/App.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import "./App.css";
|
||||
import FirecrawlComponent from "./components/ingestion";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<FirecrawlComponent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
1
apps/ui/ingestion-ui/src/assets/react.svg
Normal file
1
apps/ui/ingestion-ui/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
598
apps/ui/ingestion-ui/src/components/ingestion.tsx
Normal file
598
apps/ui/ingestion-ui/src/components/ingestion.tsx
Normal file
@ -0,0 +1,598 @@
|
||||
import { useState, ChangeEvent, FormEvent, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
// Hardcoded values (not recommended for production)
|
||||
const FIRECRAWL_API_URL = "https://api.firecrawl.dev"; // Replace with your actual API URL whether it is local or using Firecrawl Cloud
|
||||
const FIRECRAWL_API_KEY = ""; // Replace with your actual API key
|
||||
|
||||
interface FormData {
|
||||
url: string;
|
||||
crawlSubPages: boolean;
|
||||
limit: string;
|
||||
maxDepth: string;
|
||||
excludePaths: string;
|
||||
includePaths: string;
|
||||
extractMainContent: boolean;
|
||||
}
|
||||
|
||||
interface CrawlerOptions {
|
||||
includes?: string[];
|
||||
excludes?: string[];
|
||||
maxDepth?: number;
|
||||
limit?: number;
|
||||
returnOnlyUrls: boolean;
|
||||
}
|
||||
|
||||
interface PageOptions {
|
||||
onlyMainContent: boolean;
|
||||
}
|
||||
|
||||
interface RequestBody {
|
||||
url: string;
|
||||
crawlerOptions?: CrawlerOptions;
|
||||
pageOptions: PageOptions;
|
||||
}
|
||||
|
||||
interface ScrapeResultMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
language: string;
|
||||
sourceURL: string;
|
||||
pageStatusCode: number;
|
||||
pageError?: string;
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
interface ScrapeResultData {
|
||||
markdown: string;
|
||||
content: string;
|
||||
html: string;
|
||||
rawHtml: string;
|
||||
metadata: ScrapeResultMetadata;
|
||||
llm_extraction: Record<string, unknown>;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
interface ScrapeResult {
|
||||
success: boolean;
|
||||
data: ScrapeResultData;
|
||||
}
|
||||
|
||||
export default function FirecrawlComponent() {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
url: "",
|
||||
crawlSubPages: false,
|
||||
limit: "",
|
||||
maxDepth: "",
|
||||
excludePaths: "",
|
||||
includePaths: "",
|
||||
extractMainContent: false,
|
||||
});
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [scrapingSelectedLoading, setScrapingSelectedLoading] =
|
||||
useState<boolean>(false);
|
||||
const [crawledUrls, setCrawledUrls] = useState<string[]>([]);
|
||||
const [selectedUrls, setSelectedUrls] = useState<string[]>([]);
|
||||
const [scrapeResults, setScrapeResults] = useState<
|
||||
Record<string, ScrapeResult>
|
||||
>({});
|
||||
const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(true);
|
||||
const [crawlStatus, setCrawlStatus] = useState<{
|
||||
current: number;
|
||||
total: number | null;
|
||||
}>({ current: 0, total: null });
|
||||
const [elapsedTime, setElapsedTime] = useState<number>(0);
|
||||
const [showCrawlStatus, setShowCrawlStatus] = useState<boolean>(false);
|
||||
const [isScraping, setIsScraping] = useState<boolean>(false);
|
||||
const [showAllUrls, setShowAllUrls] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (loading) {
|
||||
setShowCrawlStatus(true);
|
||||
timer = setInterval(() => {
|
||||
setElapsedTime((prevTime) => prevTime + 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [loading]);
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: type === "checkbox" ? checked : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setIsCollapsibleOpen(false);
|
||||
setElapsedTime(0);
|
||||
setCrawlStatus({ current: 0, total: null });
|
||||
setIsScraping(!formData.crawlSubPages);
|
||||
setCrawledUrls([]);
|
||||
setSelectedUrls([]);
|
||||
setScrapeResults({});
|
||||
setScrapingSelectedLoading(false);
|
||||
setShowCrawlStatus(false);
|
||||
|
||||
try {
|
||||
const endpoint = `${FIRECRAWL_API_URL}/v0/${
|
||||
formData.crawlSubPages ? "crawl" : "scrape"
|
||||
}`;
|
||||
|
||||
const requestBody: RequestBody = formData.crawlSubPages
|
||||
? {
|
||||
url: formData.url,
|
||||
crawlerOptions: {
|
||||
includes: formData.includePaths
|
||||
? formData.includePaths.split(",").map((p) => p.trim())
|
||||
: undefined,
|
||||
excludes: formData.excludePaths
|
||||
? formData.excludePaths.split(",").map((p) => p.trim())
|
||||
: undefined,
|
||||
maxDepth: formData.maxDepth
|
||||
? parseInt(formData.maxDepth)
|
||||
: undefined,
|
||||
limit: formData.limit ? parseInt(formData.limit) : undefined,
|
||||
returnOnlyUrls: true,
|
||||
},
|
||||
pageOptions: {
|
||||
onlyMainContent: formData.extractMainContent,
|
||||
},
|
||||
}
|
||||
: {
|
||||
url: formData.url,
|
||||
pageOptions: {
|
||||
onlyMainContent: formData.extractMainContent,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${FIRECRAWL_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (formData.crawlSubPages) {
|
||||
const jobId = data.jobId;
|
||||
if (jobId) {
|
||||
const statusEndpoint = `${FIRECRAWL_API_URL}/v0/crawl/status/${jobId}`;
|
||||
let statusData: {
|
||||
status: string;
|
||||
data?: { url: string }[];
|
||||
current?: number;
|
||||
total?: number;
|
||||
};
|
||||
do {
|
||||
const statusResponse = await fetch(statusEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${FIRECRAWL_API_KEY}`,
|
||||
},
|
||||
});
|
||||
if (statusResponse.ok) {
|
||||
statusData = await statusResponse.json();
|
||||
|
||||
const urls = statusData.data
|
||||
? statusData.data.map((urlObj) => urlObj.url)
|
||||
: [];
|
||||
setCrawledUrls(urls);
|
||||
setSelectedUrls(urls);
|
||||
setCrawlStatus({
|
||||
current: urls.length || 0,
|
||||
total: urls.length || null,
|
||||
});
|
||||
if (statusData.status !== "completed") {
|
||||
// Wait for 1 second before polling again
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
console.log("Polling again...");
|
||||
console.log(statusData);
|
||||
} else {
|
||||
console.log("Crawl completed with status:", statusData.status);
|
||||
console.log(statusData);
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch crawl status");
|
||||
break;
|
||||
}
|
||||
} while (statusData.status !== "completed");
|
||||
} else {
|
||||
console.error("No jobId received from crawl request");
|
||||
}
|
||||
} else {
|
||||
setScrapeResults({ [formData.url]: data });
|
||||
setCrawlStatus({ current: 1, total: 1 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
setScrapeResults({
|
||||
error: {
|
||||
success: false,
|
||||
data: {
|
||||
metadata: {
|
||||
pageError: "Error occurred while fetching data",
|
||||
title: "",
|
||||
description: "",
|
||||
language: "",
|
||||
sourceURL: "",
|
||||
pageStatusCode: 0,
|
||||
},
|
||||
markdown: "",
|
||||
content: "",
|
||||
html: "",
|
||||
rawHtml: "",
|
||||
llm_extraction: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrapeSelected = async () => {
|
||||
setLoading(true);
|
||||
setElapsedTime(0);
|
||||
setCrawlStatus({ current: 0, total: selectedUrls.length });
|
||||
setIsScraping(true);
|
||||
setScrapingSelectedLoading(true);
|
||||
const newScrapeResults: Record<string, ScrapeResult> = {};
|
||||
|
||||
for (const [index, url] of selectedUrls.entries()) {
|
||||
try {
|
||||
const response = await fetch(`${FIRECRAWL_API_URL}/v0/scrape`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${FIRECRAWL_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
pageOptions: {
|
||||
onlyMainContent: formData.extractMainContent,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ScrapeResult = await response.json();
|
||||
newScrapeResults[url] = data;
|
||||
setCrawlStatus((prev) => ({ ...prev, current: index + 1 }));
|
||||
} catch (error) {
|
||||
console.error(`Error scraping ${url}:`, error);
|
||||
newScrapeResults[url] = {
|
||||
success: false,
|
||||
data: {
|
||||
markdown: "",
|
||||
content: "",
|
||||
html: "",
|
||||
rawHtml: "",
|
||||
metadata: {
|
||||
title: "",
|
||||
description: "",
|
||||
language: "",
|
||||
sourceURL: url,
|
||||
pageStatusCode: 0,
|
||||
pageError: (error as Error).message,
|
||||
},
|
||||
llm_extraction: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setScrapeResults(newScrapeResults);
|
||||
setLoading(false);
|
||||
setIsScraping(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-4">
|
||||
<Card>
|
||||
<CardHeader className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<span>Extract web content with Firecrawl 🔥</span>
|
||||
</CardTitle>
|
||||
<div className="text-sm text-gray-500 w-11/12 items-center">
|
||||
Use this component to quickly build your own UI for Firecrawl. Plug
|
||||
in your API key and the component will handle the rest. Learn more
|
||||
on the{" "}
|
||||
<a
|
||||
href="https://docs.firecrawl.dev/"
|
||||
className="text-sm text-blue-500"
|
||||
>
|
||||
Firecrawl docs!
|
||||
</a>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="https://www.firecrawl.dev/"
|
||||
className="flex-grow"
|
||||
name="url"
|
||||
value={formData.url}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button type="submit" variant="default" disabled={loading}>
|
||||
{loading ? "Running..." : "Run"}
|
||||
</Button>
|
||||
</div>
|
||||
<Collapsible
|
||||
open={isCollapsibleOpen}
|
||||
onOpenChange={setIsCollapsibleOpen}
|
||||
className="mt-2"
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" className="w-full justify-between pl-2">
|
||||
Advanced Options
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 mt-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="crawlSubPages"
|
||||
name="crawlSubPages"
|
||||
checked={formData.crawlSubPages}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
crawlSubPages: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label htmlFor="crawlSubPages" className="text-sm">
|
||||
Crawl sub-pages
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="limit"
|
||||
className="block text-left w-full pb-2"
|
||||
>
|
||||
Limit *
|
||||
</Label>
|
||||
<Input
|
||||
id="limit"
|
||||
name="limit"
|
||||
placeholder="10"
|
||||
value={formData.limit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="maxDepth"
|
||||
className="block text-left w-full pb-2"
|
||||
>
|
||||
Max depth
|
||||
</Label>
|
||||
<Input
|
||||
id="maxDepth"
|
||||
name="maxDepth"
|
||||
placeholder="5"
|
||||
value={formData.maxDepth}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="excludePaths"
|
||||
className="block text-left w-full pb-2"
|
||||
>
|
||||
Exclude paths
|
||||
</Label>
|
||||
<Input
|
||||
id="excludePaths"
|
||||
name="excludePaths"
|
||||
placeholder="blog/, /about/"
|
||||
value={formData.excludePaths}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="includePaths"
|
||||
className="block text-left w-full pb-2"
|
||||
>
|
||||
Include only paths
|
||||
</Label>
|
||||
<Input
|
||||
id="includePaths"
|
||||
name="includePaths"
|
||||
placeholder="articles/"
|
||||
value={formData.includePaths}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="extractMainContent"
|
||||
name="extractMainContent"
|
||||
checked={formData.extractMainContent}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
extractMainContent: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label htmlFor="extractMainContent" className="text-sm">
|
||||
Extract only main content (no headers, navs, footers, etc.)
|
||||
</label>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</form>
|
||||
{showCrawlStatus && (
|
||||
<div className="flex items-center justify-between mb-2 space-x-2 bg-gray-100 p-2 rounded-md">
|
||||
<div className="flex items-center space-x-2">
|
||||
{!isScraping &&
|
||||
crawledUrls.length > 0 &&
|
||||
!scrapingSelectedLoading && (
|
||||
<>
|
||||
<Checkbox
|
||||
id="selectAll"
|
||||
checked={selectedUrls.length === crawledUrls.length}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedUrls([...crawledUrls]);
|
||||
} else {
|
||||
setSelectedUrls([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="selectAll"
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
{selectedUrls.length === crawledUrls.length
|
||||
? "Unselect All"
|
||||
: "Select All"}
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{isScraping
|
||||
? `Scraped ${crawlStatus.current} page(s) in ${elapsedTime}s`
|
||||
: `Crawled ${crawlStatus.current} pages in ${elapsedTime}s`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{crawledUrls.length > 0 &&
|
||||
!scrapingSelectedLoading &&
|
||||
!isScraping && (
|
||||
<>
|
||||
<ul className="pl-2">
|
||||
{(showAllUrls ? crawledUrls : crawledUrls.slice(0, 10)).map(
|
||||
(url, index) => (
|
||||
<li key={index} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={selectedUrls.includes(url)}
|
||||
onCheckedChange={() =>
|
||||
setSelectedUrls((prev) =>
|
||||
prev.includes(url)
|
||||
? prev.filter((u) => u !== url)
|
||||
: [...prev, url]
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>{url}</span>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
{crawledUrls.length > 10 && (
|
||||
<div className="flex justify-center mt-2">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => setShowAllUrls(!showAllUrls)}
|
||||
>
|
||||
{showAllUrls ? "Show Less" : "Show All"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-center">
|
||||
{crawledUrls.length > 0 && !scrapingSelectedLoading && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleScrapeSelected}
|
||||
disabled={loading || selectedUrls.length === 0}
|
||||
>
|
||||
Scrape Selected URLs
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{Object.keys(scrapeResults).length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h2 className="text-2xl font-bold mb-4">Scrape Results</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{Object.entries(scrapeResults).map(([url, result]) => (
|
||||
<Card key={url} className="overflow-hidden">
|
||||
<CardContent>
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
<div className="text-base font-bold py-2">
|
||||
{url
|
||||
.replace(/^(https?:\/\/)?(www\.)?/, "")
|
||||
.replace(/\/$/, "")}
|
||||
</div>
|
||||
{result.success ? (
|
||||
<>
|
||||
<pre className="text-xs whitespace-pre-wrap">
|
||||
{result.data.markdown.trim().slice(0, 200)}...
|
||||
</pre>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-red-500">Failed to scrape this URL</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(event) => {
|
||||
navigator.clipboard.writeText(result.data.markdown);
|
||||
const button = event.currentTarget as HTMLButtonElement;
|
||||
const originalText = button.textContent;
|
||||
button.textContent = "Copied!";
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
Copy Markdown
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
56
apps/ui/ingestion-ui/src/components/ui/button.tsx
Normal file
56
apps/ui/ingestion-ui/src/components/ui/button.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
79
apps/ui/ingestion-ui/src/components/ui/card.tsx
Normal file
79
apps/ui/ingestion-ui/src/components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
28
apps/ui/ingestion-ui/src/components/ui/checkbox.tsx
Normal file
28
apps/ui/ingestion-ui/src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
9
apps/ui/ingestion-ui/src/components/ui/collapsible.tsx
Normal file
9
apps/ui/ingestion-ui/src/components/ui/collapsible.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
25
apps/ui/ingestion-ui/src/components/ui/input.tsx
Normal file
25
apps/ui/ingestion-ui/src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
24
apps/ui/ingestion-ui/src/components/ui/label.tsx
Normal file
24
apps/ui/ingestion-ui/src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
69
apps/ui/ingestion-ui/src/index.css
Normal file
69
apps/ui/ingestion-ui/src/index.css
Normal file
@ -0,0 +1,69 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
6
apps/ui/ingestion-ui/src/lib/utils.ts
Normal file
6
apps/ui/ingestion-ui/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
10
apps/ui/ingestion-ui/src/main.tsx
Normal file
10
apps/ui/ingestion-ui/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
1
apps/ui/ingestion-ui/src/vite-env.d.ts
vendored
Normal file
1
apps/ui/ingestion-ui/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
77
apps/ui/ingestion-ui/tailwind.config.js
Normal file
77
apps/ui/ingestion-ui/tailwind.config.js
Normal file
@ -0,0 +1,77 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
33
apps/ui/ingestion-ui/tsconfig.app.json
Normal file
33
apps/ui/ingestion-ui/tsconfig.app.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
17
apps/ui/ingestion-ui/tsconfig.json
Normal file
17
apps/ui/ingestion-ui/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
13
apps/ui/ingestion-ui/tsconfig.node.json
Normal file
13
apps/ui/ingestion-ui/tsconfig.node.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
12
apps/ui/ingestion-ui/vite.config.ts
Normal file
12
apps/ui/ingestion-ui/vite.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import path from "path"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user