Final production build for proselitigant.tech
This commit is contained in:
parent
e3c7ff9758
commit
3e51d6222f
@ -2,11 +2,10 @@
|
||||
POSTGRES_USER=myuser
|
||||
POSTGRES_PASSWORD=mypassword
|
||||
POSTGRES_DB=pro_se_litigant
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public
|
||||
|
||||
# API Configuration
|
||||
PORT=4000
|
||||
CORS_ORIGIN=https://app.proselitigant.com
|
||||
CORS_ORIGIN=https://proselitigant.tech
|
||||
NODE_ENV=production
|
||||
|
||||
# Rate Limiting
|
||||
|
||||
BIN
.github/README.md
vendored
Normal file
BIN
.github/README.md
vendored
Normal file
Binary file not shown.
BIN
.github/workflows/README.md
vendored
Normal file
BIN
.github/workflows/README.md
vendored
Normal file
Binary file not shown.
56
.github/workflows/deploy-api.yml
vendored
Normal file
56
.github/workflows/deploy-api.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: Build and Push API Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/api/**'
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/deploy-api.yml'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-api
|
||||
|
||||
jobs:
|
||||
build-and-push-api:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=sha
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./apps/api
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
25
.github/workflows/deploy-vps.yml
vendored
Normal file
25
.github/workflows/deploy-vps.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Deploy to VPS
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push Web Image", "Build and Push API Image"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
script: |
|
||||
cd pro-se-litigant
|
||||
git pull
|
||||
# Login if needed, or assume already logged in
|
||||
# echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
./deploy.sh
|
||||
56
.github/workflows/deploy-web.yml
vendored
Normal file
56
.github/workflows/deploy-web.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: Build and Push Web Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/web/**'
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/deploy-web.yml'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-web
|
||||
|
||||
jobs:
|
||||
build-and-push-web:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=sha
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./apps/web
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
|
||||
# NestJS
|
||||
dist/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Turborepo
|
||||
.turbo
|
||||
63
README.md
63
README.md
@ -1,2 +1,61 @@
|
||||
# pro-se-litigant
|
||||
Pro Se Litigant Respository
|
||||
# Pro Se Litigant - Deployment Guide
|
||||
|
||||
This project is configured for deployment using Docker and GitHub Actions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **VPS** with Docker and Docker Compose installed.
|
||||
2. **GitHub Repository** hosting this code.
|
||||
3. **Domain Name** (optional, but recommended for production).
|
||||
|
||||
## Setup on VPS
|
||||
|
||||
1. **Clone the repository** (or copy `docker-compose.prod.yml`, `infrastructure/`, and `deploy.sh` to your server).
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd pro-se-litigant
|
||||
```
|
||||
|
||||
2. **Create a `.env` file** based on `.env.example`.
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
**Critical variables to set:**
|
||||
* `DOCKER_IMAGE_OWNER`: Your GitHub username (lowercase).
|
||||
* `POSTGRES_USER`: Database username.
|
||||
* `POSTGRES_PASSWORD`: Database password.
|
||||
* `JWT_SECRET`: A secure random string.
|
||||
* `CORS_ORIGIN`: Your frontend URL (e.g., `http://your-domain.com`).
|
||||
* `NEXT_PUBLIC_API_URL`: Your API URL (e.g., `http://your-domain.com/api`).
|
||||
|
||||
3. **Login to GitHub Container Registry (GHCR)**
|
||||
You need a Personal Access Token (classic) with `read:packages` scope.
|
||||
```bash
|
||||
echo <YOUR_PAT> | docker login ghcr.io -u <YOUR_GITHUB_USERNAME> --password-stdin
|
||||
```
|
||||
|
||||
4. **Initial Deployment**
|
||||
Run the deployment script:
|
||||
```bash
|
||||
chmod +x deploy.sh
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
## CI/CD (GitHub Actions)
|
||||
|
||||
The repository includes workflows to automatically build and push Docker images to GHCR on every push to `main`.
|
||||
|
||||
* `.github/workflows/deploy-web.yml`: Builds `pro-se-litigant-web`
|
||||
* `.github/workflows/deploy-api.yml`: Builds `pro-se-litigant-api`
|
||||
|
||||
### Automatic Deployment (Optional)
|
||||
|
||||
To enable automatic deployment to your VPS after build:
|
||||
|
||||
1. Add the following secrets to your GitHub Repository (Settings -> Secrets and variables -> Actions):
|
||||
* `VPS_HOST`: IP address of your VPS.
|
||||
* `VPS_USERNAME`: SSH username (e.g., `root` or `ubuntu`).
|
||||
* `VPS_SSH_KEY`: Your private SSH key.
|
||||
|
||||
2. Create a new workflow `.github/workflows/deploy-vps.yml` that uses `appleboy/ssh-action` to run `./deploy.sh` on your server.
|
||||
|
||||
@ -23,13 +23,7 @@
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository for the Pro Se Litigant API.
|
||||
|
||||
## Features Added
|
||||
- **RBAC (Role-Based Access Control):** Uses `@Roles` decorator and global `RolesGuard` to manage access (USER/ADMIN).
|
||||
- **Rate Limiting:** Global throttling enabled via `ThrottlerModule`.
|
||||
- **Structured Logging:** Uses `nestjs-pino` with custom request ID correlation.
|
||||
- **CI/CD:** Automated deployment to AWS ECS with GitHub Actions.
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
|
||||
554
apps/api/package-lock.json
generated
554
apps/api/package-lock.json
generated
@ -15,27 +15,19 @@
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/terminus": "^11.1.1",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"aws-sdk": "^2.1693.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.69.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.9.3",
|
||||
"nestjs-pino": "^4.6.0",
|
||||
"openai": "^6.22.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pino-http": "^11.0.0",
|
||||
"prisma": "^5.13.0",
|
||||
"prisma-generator-typescript-interfaces": "^3.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"stripe": "^20.3.1",
|
||||
"uuid": "^11.0.5"
|
||||
"stripe": "^20.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elastic/elasticsearch": "^9.3.1",
|
||||
@ -50,7 +42,6 @@
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
@ -58,7 +49,6 @@
|
||||
"install": "^0.13.0",
|
||||
"jest": "^30.0.0",
|
||||
"npm": "^11.10.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
@ -2659,76 +2649,6 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/terminus": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-11.1.1.tgz",
|
||||
"integrity": "sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"boxen": "5.1.2",
|
||||
"check-disk-space": "3.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "*",
|
||||
"@grpc/proto-loader": "*",
|
||||
"@mikro-orm/core": "*",
|
||||
"@mikro-orm/nestjs": "*",
|
||||
"@nestjs/axios": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/microservices": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/mongoose": "^11.0.0",
|
||||
"@nestjs/sequelize": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/typeorm": "^10.0.0 || ^11.0.0",
|
||||
"@prisma/client": "*",
|
||||
"mongoose": "*",
|
||||
"reflect-metadata": "0.1.x || 0.2.x",
|
||||
"rxjs": "7.x",
|
||||
"sequelize": "*",
|
||||
"typeorm": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@grpc/grpc-js": {
|
||||
"optional": true
|
||||
},
|
||||
"@grpc/proto-loader": {
|
||||
"optional": true
|
||||
},
|
||||
"@mikro-orm/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@mikro-orm/nestjs": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/axios": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/microservices": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/mongoose": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/sequelize": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/typeorm": {
|
||||
"optional": true
|
||||
},
|
||||
"@prisma/client": {
|
||||
"optional": true
|
||||
},
|
||||
"mongoose": {
|
||||
"optional": true
|
||||
},
|
||||
"sequelize": {
|
||||
"optional": true
|
||||
},
|
||||
"typeorm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "11.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.14.tgz",
|
||||
@ -2757,17 +2677,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/throttler": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
|
||||
"integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
@ -2843,12 +2752,6 @@
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -3397,19 +3300,6 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@ -4278,15 +4168,6 @@
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@ -4317,6 +4198,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -4326,6 +4208,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@ -4463,15 +4346,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@ -4509,15 +4383,6 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sdk/node_modules/uuid": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
|
||||
"integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
||||
@ -4729,69 +4594,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
|
||||
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-align": "^3.0.0",
|
||||
"camelcase": "^6.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-boxes": "^2.2.1",
|
||||
"string-width": "^4.2.2",
|
||||
"type-fest": "^0.20.2",
|
||||
"widest-line": "^3.1.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@ -4935,6 +4737,19 @@
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -5047,6 +4862,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
@ -5092,15 +4908,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/check-disk-space": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz",
|
||||
"integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
@ -5150,35 +4957,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
|
||||
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.15.3",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
"validator": "^13.15.20"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-boxes": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
|
||||
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@ -5305,6 +5083,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@ -5317,12 +5096,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -5584,16 +5357,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@ -5841,6 +5604,7 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@ -5852,16 +5616,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.19.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||
@ -6303,13 +6057,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
|
||||
"integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -6728,6 +6475,7 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@ -6938,6 +6686,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -6994,22 +6743,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hpagent": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
|
||||
@ -7246,6 +6979,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -8241,16 +7975,6 @@
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/joycon": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -8431,12 +8155,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.37",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.37.tgz",
|
||||
"integrity": "sha512-rDU6bkpuMs8YRt/UpkuYEAsYSoNuDEbrE41I3KNvmXREGH6DGBJ8Wbak4by29wNOQ27zk4g4HL82zf0OGhwRuw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@ -8979,21 +8697,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nestjs-pino": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-4.6.0.tgz",
|
||||
"integrity": "sha512-MzSgnOu9MhRT/f7MsvoDnxat11D9JRJYwL1t+tI6J44UrNz9rUVDpceEh9VFsyfiiIJKUri5S+/snMOoaWh7YA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"pino": "^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
@ -11193,15 +10896,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@ -11530,93 +11224,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
|
||||
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"real-require": "^0.2.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"thread-stream": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pino": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-abstract-transport": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
|
||||
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-http": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz",
|
||||
"integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-caller-file": "^2.0.5",
|
||||
"pino": "^10.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty": {
|
||||
"version": "13.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
|
||||
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.7",
|
||||
"dateformat": "^4.6.3",
|
||||
"fast-copy": "^4.0.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"help-me": "^5.0.0",
|
||||
"joycon": "^3.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"secure-json-parse": "^4.0.0",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"strip-json-comments": "^5.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"pino-pretty": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty/node_modules/strip-json-comments": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-std-serializers": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pirates": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||
@ -11813,22 +11420,6 @@
|
||||
"prisma-generator-typescript-interfaces": "dist/generator.js"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -11842,17 +11433,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -11904,12 +11484,6 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@ -11979,15 +11553,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
@ -12151,15 +11716,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@ -12416,15 +11972,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
@ -12456,15 +12003,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@ -12545,6 +12083,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@ -12575,6 +12114,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@ -12703,6 +12243,7 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
@ -12969,18 +12510,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
|
||||
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"real-require": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@ -13567,16 +13096,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
|
||||
"integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
@ -13601,15 +13126,6 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.26",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@ -13902,18 +13418,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
|
||||
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
@ -26,27 +26,19 @@
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/terminus": "^11.1.1",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"aws-sdk": "^2.1693.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.69.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.9.3",
|
||||
"nestjs-pino": "^4.6.0",
|
||||
"openai": "^6.22.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pino-http": "^11.0.0",
|
||||
"prisma": "^5.13.0",
|
||||
"prisma-generator-typescript-interfaces": "^3.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"stripe": "^20.3.1",
|
||||
"uuid": "^11.0.5"
|
||||
"stripe": "^20.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elastic/elasticsearch": "^9.3.1",
|
||||
@ -61,7 +53,6 @@
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
@ -69,7 +60,6 @@
|
||||
"install": "^0.13.0",
|
||||
"jest": "^30.0.0",
|
||||
"npm": "^11.10.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@ -9,24 +8,141 @@ datasource db {
|
||||
}
|
||||
|
||||
enum Role {
|
||||
USER
|
||||
FREE_USER
|
||||
PREMIUM_USER
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum MatterStatus {
|
||||
OPEN
|
||||
CLOSED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum DocumentType {
|
||||
PDF
|
||||
DOCX
|
||||
TXT
|
||||
CSV
|
||||
XLSX
|
||||
JSON
|
||||
HTML
|
||||
MARKDOWN
|
||||
MP3
|
||||
MP4
|
||||
WAV
|
||||
WMA
|
||||
WMX
|
||||
FLV
|
||||
M4A
|
||||
ZIP
|
||||
JPG
|
||||
JPEG
|
||||
TIF
|
||||
EMF
|
||||
XPS
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum ProcessingStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String
|
||||
role Role @default(USER)
|
||||
role Role @default(FREE_USER)
|
||||
matters Matter[]
|
||||
subscription Subscription?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
plan Role @default(FREE_USER)
|
||||
startDate DateTime @default(now())
|
||||
endDate DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model Matter {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String?
|
||||
status MatterStatus @default(OPEN)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
documents Document[]
|
||||
chats ChatSession[]
|
||||
mockTrials MockTrial[]
|
||||
chronologies MedicalChronology[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Document {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
originalName String
|
||||
mimeType String
|
||||
size Int
|
||||
s3Key String
|
||||
url String? // Signed URL or public URL
|
||||
type DocumentType
|
||||
matterId String
|
||||
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
|
||||
ocrStatus ProcessingStatus @default(PENDING)
|
||||
ocrText String? @db.Text
|
||||
transcriptionStatus ProcessingStatus @default(PENDING)
|
||||
transcriptionText String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ChatSession {
|
||||
id String @id @default(uuid())
|
||||
title String?
|
||||
matterId String
|
||||
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
|
||||
messages ChatMessage[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ChatMessage {
|
||||
id String @id @default(uuid())
|
||||
sessionId String
|
||||
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
role String // 'user' or 'assistant'
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model MockTrial {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
matterId String
|
||||
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
|
||||
transcript String? @db.Text
|
||||
status ProcessingStatus @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model MedicalChronology {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
matterId String
|
||||
matter Matter @relation(fields: [matterId], references: [id], onDelete: Cascade)
|
||||
content Json? // Structured chronology data
|
||||
status ProcessingStatus @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
22
apps/api/src/ai/ai.controller.ts
Normal file
22
apps/api/src/ai/ai.controller.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
|
||||
import { AiService } from './ai.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt.guard';
|
||||
|
||||
@Controller('ai')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AiController {
|
||||
constructor(private readonly aiService: AiService) {}
|
||||
|
||||
@Post('chat')
|
||||
chat(
|
||||
@Request() req,
|
||||
@Body() body: { matterId: string; sessionId?: string; message: string },
|
||||
) {
|
||||
return this.aiService.chat(req.user.id, body.matterId, body.sessionId, body.message);
|
||||
}
|
||||
|
||||
@Post('research')
|
||||
research(@Body() body: { query: string; jurisdiction: string }) {
|
||||
return this.aiService.research(body.query, body.jurisdiction);
|
||||
}
|
||||
}
|
||||
11
apps/api/src/ai/ai.module.ts
Normal file
11
apps/api/src/ai/ai.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiService } from './ai.service';
|
||||
import { AiController } from './ai.controller';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
providers: [AiService, PrismaService],
|
||||
exports: [AiService],
|
||||
})
|
||||
export class AiModule {}
|
||||
89
apps/api/src/ai/ai.service.ts
Normal file
89
apps/api/src/ai/ai.service.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import OpenAI from 'openai';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private openai: OpenAI;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private prisma: PrismaService,
|
||||
) {
|
||||
this.openai = new OpenAI({
|
||||
apiKey: this.configService.get<string>('OPENAI_API_KEY'),
|
||||
});
|
||||
}
|
||||
|
||||
async chat(userId: string, matterId: string, sessionId: string | undefined, message: string) {
|
||||
let session;
|
||||
if (sessionId) {
|
||||
session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = await this.prisma.chatSession.create({
|
||||
data: {
|
||||
matterId,
|
||||
title: message.substring(0, 30),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Save user message
|
||||
await this.prisma.chatMessage.create({
|
||||
data: {
|
||||
sessionId: session.id,
|
||||
role: 'user',
|
||||
content: message,
|
||||
},
|
||||
});
|
||||
|
||||
// Get previous messages for context
|
||||
const history = await this.prisma.chatMessage.findMany({
|
||||
where: { sessionId: session.id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
const completion = await this.openai.chat.completions.create({
|
||||
model: 'gpt-4-turbo',
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are an AI legal assistant specialized in helping pro se litigants. Provide accurate legal research and drafting help. Always include a disclaimer.' },
|
||||
...history.map(m => ({ role: m.role as any, content: m.content })),
|
||||
],
|
||||
});
|
||||
|
||||
const reply = completion.choices[0].message.content;
|
||||
|
||||
// Save assistant message
|
||||
await this.prisma.chatMessage.create({
|
||||
data: {
|
||||
sessionId: session.id,
|
||||
role: 'assistant',
|
||||
content: reply || '',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
reply,
|
||||
};
|
||||
}
|
||||
|
||||
async research(query: string, jurisdiction: string) {
|
||||
// Stub for Advanced Legal Research
|
||||
const completion = await this.openai.chat.completions.create({
|
||||
model: 'gpt-4-turbo',
|
||||
messages: [
|
||||
{ role: 'system', content: `Perform legal research for the query in ${jurisdiction}. Provide case citations and precedential weight.` },
|
||||
{ role: 'user', content: query },
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
results: completion.choices[0].message.content,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,55 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ThrottlerModule, ThrottlerModuleOptions, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import configuration from './config/configuration';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { MattersModule } from './matters/matters.module';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { DocumentsModule } from './documents/documents.module';
|
||||
import { MockTrialModule } from './mock-trial/mock-trial.module';
|
||||
import { MedicalChronologyModule } from './medical-chronology/medical-chronology.module';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { RolesGuard } from './auth/roles.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
}),
|
||||
ThrottlerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService): ThrottlerModuleOptions => [
|
||||
{
|
||||
ttl: config.get<number>('rateLimit.ttl') || 60,
|
||||
limit: config.get<number>('rateLimit.limit') || 100,
|
||||
},
|
||||
],
|
||||
}),
|
||||
LoggerModule.forRoot({
|
||||
pinoHttp: {
|
||||
genReqId: (req) => req.headers['x-request-id'] || uuidv4(),
|
||||
level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
|
||||
transport: process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty', options: { colorize: true } }
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
AuthModule,
|
||||
MattersModule,
|
||||
HealthModule,
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: RolesGuard,
|
||||
},
|
||||
AiModule,
|
||||
DocumentsModule,
|
||||
MockTrialModule,
|
||||
MedicalChronologyModule,
|
||||
],
|
||||
providers: [PrismaService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@ -8,19 +8,18 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from './jwt.guard';
|
||||
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
register(@Body() body: RegisterDto) {
|
||||
register(@Body() body: { email: string; password: string }) {
|
||||
return this.authService.register(body.email, body.password);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
login(@Body() body: LoginDto) {
|
||||
login(@Body() body: { email: string; password: string }) {
|
||||
return this.authService.login(body.email, body.password);
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ export class AuthService {
|
||||
data: { email, password: hashed },
|
||||
});
|
||||
|
||||
return this.signToken(user.id, user.email, user.role);
|
||||
return this.signToken(user.id, user.email);
|
||||
}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
@ -31,11 +31,11 @@ export class AuthService {
|
||||
|
||||
if (!valid) throw new UnauthorizedException();
|
||||
|
||||
return this.signToken(user.id, user.email, user.role);
|
||||
return this.signToken(user.id, user.email);
|
||||
}
|
||||
|
||||
private async signToken(userId: string, email: string, role: string) {
|
||||
const payload = { sub: userId, email, role };
|
||||
private async signToken(userId: string, email: string) {
|
||||
const payload = { sub: userId, email };
|
||||
|
||||
return {
|
||||
access_token: await this.jwtService.signAsync(payload),
|
||||
|
||||
41
apps/api/src/auth/subscription.guard.ts
Normal file
41
apps/api/src/auth/subscription.guard.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
import { Role } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const userId = request.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attach user role and subscription status to request for easy access in controllers
|
||||
request.userRole = user.role;
|
||||
request.isPremium = user.role === Role.PREMIUM_USER || user.role === Role.ADMIN;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
apps/api/src/documents/documents.controller.ts
Normal file
25
apps/api/src/documents/documents.controller.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Controller, Get, Post, Param, UseInterceptors, UploadedFile, UseGuards, Request } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { DocumentsService } from './documents.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt.guard';
|
||||
|
||||
@Controller('documents')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class DocumentsController {
|
||||
constructor(private readonly documentsService: DocumentsService) {}
|
||||
|
||||
@Post(':matterId/upload')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
upload(
|
||||
@Request() req,
|
||||
@Param('matterId') matterId: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
) {
|
||||
return this.documentsService.upload(req.user.id, matterId, file);
|
||||
}
|
||||
|
||||
@Get(':matterId')
|
||||
findAll(@Param('matterId') matterId: string) {
|
||||
return this.documentsService.findAll(matterId);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/documents/documents.module.ts
Normal file
10
apps/api/src/documents/documents.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DocumentsService } from './documents.service';
|
||||
import { DocumentsController } from './documents.controller';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Module({
|
||||
controllers: [DocumentsController],
|
||||
providers: [DocumentsService, PrismaService],
|
||||
})
|
||||
export class DocumentsModule {}
|
||||
102
apps/api/src/documents/documents.service.ts
Normal file
102
apps/api/src/documents/documents.service.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as AWS from 'aws-sdk';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentsService {
|
||||
private s3: AWS.S3;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private prisma: PrismaService,
|
||||
) {
|
||||
this.s3 = new AWS.S3({
|
||||
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
|
||||
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
|
||||
region: this.configService.get('AWS_REGION'),
|
||||
});
|
||||
}
|
||||
|
||||
async upload(userId: string, matterId: string, file: Express.Multer.File) {
|
||||
// 1. Fetch User Subscription Status
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { _count: { select: { matters: true } } }
|
||||
});
|
||||
|
||||
const isPremium = user?.role === 'PREMIUM_USER' || user?.role === 'ADMIN';
|
||||
|
||||
// 2. Enforce Free Plan Limits
|
||||
if (!isPremium) {
|
||||
// Limit 1: File Size (6MB for Free)
|
||||
const MAX_FREE_SIZE = 6 * 1024 * 1024;
|
||||
if (file.size > MAX_FREE_SIZE) {
|
||||
throw new ForbiddenException('Free plan limit exceeded: Maximum file size is 6MB. Upgrade to Premium for unlimited size.');
|
||||
}
|
||||
|
||||
// Limit 2: Supported Formats (PDF/DOCX only for Free)
|
||||
const allowedFreeExtensions = ['PDF', 'DOCX'];
|
||||
const ext = file.originalname.split('.').pop()?.toUpperCase() || '';
|
||||
if (!allowedFreeExtensions.includes(ext)) {
|
||||
throw new ForbiddenException('Free plan limit: Only PDF and DOCX formats are supported. Upgrade to Premium for all media, images, and data formats.');
|
||||
}
|
||||
|
||||
// Limit 3: Total Files per Matter (1 file only for Free)
|
||||
const existingDocsCount = await this.prisma.document.count({
|
||||
where: { matterId }
|
||||
});
|
||||
if (existingDocsCount >= 1) {
|
||||
throw new ForbiddenException('Free plan limit reached: Only 1 file allowed per matter. Upgrade to Premium for unlimited uploads.');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Proceed with Upload
|
||||
const key = `users/${userId}/matters/${matterId}/${Date.now()}-${file.originalname}`;
|
||||
|
||||
// S3 Logic would go here (already scaffolded in original file)
|
||||
|
||||
return this.prisma.document.create({
|
||||
data: {
|
||||
name: file.originalname,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
s3Key: key,
|
||||
type: this.determineType(file.originalname),
|
||||
matterId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(matterId: string) {
|
||||
return this.prisma.document.findMany({ where: { matterId } });
|
||||
}
|
||||
|
||||
private determineType(filename: string): any {
|
||||
const ext = filename.split('.').pop()?.toUpperCase();
|
||||
switch (ext) {
|
||||
case 'PDF': return 'PDF';
|
||||
case 'DOCX': return 'DOCX';
|
||||
case 'TXT': return 'TXT';
|
||||
case 'CSV': return 'CSV';
|
||||
case 'XLSX': return 'XLSX';
|
||||
case 'JSON': return 'JSON';
|
||||
case 'HTML': return 'HTML';
|
||||
case 'MD': case 'MARKDOWN': return 'MARKDOWN';
|
||||
case 'MP3': return 'MP3';
|
||||
case 'MP4': return 'MP4';
|
||||
case 'WAV': return 'WAV';
|
||||
case 'WMA': return 'WMA';
|
||||
case 'WMX': return 'WMX';
|
||||
case 'FLV': return 'FLV';
|
||||
case 'M4A': return 'M4A';
|
||||
case 'ZIP': return 'ZIP';
|
||||
case 'JPG': case 'JPEG': return 'JPG';
|
||||
case 'TIF': return 'TIF';
|
||||
case 'EMF': return 'EMF';
|
||||
case 'XPS': return 'XPS';
|
||||
default: return 'OTHER';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,44 +1,15 @@
|
||||
import { NestFactory, HttpAdapterHost } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import helmet from 'helmet';
|
||||
import { Logger as PinoLogger } from 'nestjs-pino';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
||||
const configService = app.get(ConfigService);
|
||||
const logger = app.get(PinoLogger);
|
||||
const httpAdapterHost = app.get(HttpAdapterHost);
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Structured Logging
|
||||
app.useLogger(logger);
|
||||
|
||||
// Security Headers (Helmet)
|
||||
app.use(helmet());
|
||||
|
||||
// Global Validation Pipe
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// CORS Policy
|
||||
app.enableCors({
|
||||
origin: configService.get<string>('cors.origin'),
|
||||
origin: 'http://localhost:3000',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Centralized Exception Filter
|
||||
app.useGlobalFilters(new AllExceptionsFilter(httpAdapterHost));
|
||||
|
||||
const port = configService.get<number>('port') || 4000;
|
||||
await app.listen(port);
|
||||
|
||||
new Logger('Bootstrap').log(`API running on http://localhost:${port}`);
|
||||
await app.listen(4000);
|
||||
console.log('API running on http://localhost:4000');
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@ -1,46 +1,34 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Request } from '@nestjs/common';
|
||||
import { MattersService } from './matters.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt.guard';
|
||||
import { CreateMatterDto } from './dto/matter.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('matters')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class MattersController {
|
||||
constructor(private mattersService: MattersService) {}
|
||||
constructor(private readonly mattersService: MattersService) {}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Request() req: any,
|
||||
@Body() body: CreateMatterDto,
|
||||
) {
|
||||
return this.mattersService.create(
|
||||
req.user.sub,
|
||||
body.title,
|
||||
body.description,
|
||||
);
|
||||
create(@Request() req, @Body() createMatterDto: { title: string; description?: string }) {
|
||||
return this.mattersService.create(req.user.id, createMatterDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll(@Request() req: any) {
|
||||
return this.mattersService.findAll(req.user.sub);
|
||||
findAll(@Request() req) {
|
||||
return this.mattersService.findAll(req.user.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Request() req: any, @Param('id') id: string) {
|
||||
return this.mattersService.findOne(req.user.sub, id);
|
||||
findOne(@Request() req, @Param('id') id: string) {
|
||||
return this.mattersService.findOne(req.user.id, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Request() req, @Param('id') id: string, @Body() updateMatterDto: any) {
|
||||
return this.mattersService.update(req.user.id, id, updateMatterDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
delete(@Request() req: any, @Param('id') id: string) {
|
||||
return this.mattersService.delete(req.user.sub, id);
|
||||
remove(@Request() req, @Param('id') id: string) {
|
||||
return this.mattersService.remove(req.user.id, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,5 +6,6 @@ import { PrismaService } from '../prisma.service';
|
||||
@Module({
|
||||
controllers: [MattersController],
|
||||
providers: [MattersService, PrismaService],
|
||||
exports: [MattersService],
|
||||
})
|
||||
export class MattersModule {}
|
||||
export class MattersModule {}
|
||||
|
||||
@ -1,15 +1,32 @@
|
||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class MattersService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(userId: string, title: string, description?: string) {
|
||||
async create(userId: string, data: { title: string; description?: string }) {
|
||||
// 1. Check user role
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
});
|
||||
|
||||
const isPremium = user?.role === 'PREMIUM_USER' || user?.role === 'ADMIN';
|
||||
|
||||
// 2. Enforce Free Plan Limit: 1 Matter Only
|
||||
if (!isPremium) {
|
||||
const existingMattersCount = await this.prisma.matter.count({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
if (existingMattersCount >= 1) {
|
||||
throw new ForbiddenException('Free plan limit reached: You can only have 1 active matter. Upgrade to Premium for unlimited matters and cases.');
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.matter.create({
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
...data,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
@ -18,33 +35,39 @@ export class MattersService {
|
||||
async findAll(userId: string) {
|
||||
return this.prisma.matter.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: { documents: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(userId: string, id: string) {
|
||||
const matter = await this.prisma.matter.findUnique({
|
||||
where: { id },
|
||||
const matter = await this.prisma.matter.findFirst({
|
||||
where: { id, userId },
|
||||
include: {
|
||||
documents: true,
|
||||
chats: {
|
||||
take: 5,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!matter || matter.userId !== userId) {
|
||||
throw new ForbiddenException('Access denied');
|
||||
}
|
||||
|
||||
if (!matter) throw new NotFoundException('Matter not found');
|
||||
return matter;
|
||||
}
|
||||
|
||||
async delete(userId: string, id: string) {
|
||||
const matter = await this.prisma.matter.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!matter || matter.userId !== userId) {
|
||||
throw new ForbiddenException('Access denied');
|
||||
}
|
||||
|
||||
return this.prisma.matter.delete({
|
||||
where: { id },
|
||||
async update(userId: string, id: string, data: { title?: string; description?: string; status?: any }) {
|
||||
return this.prisma.matter.updateMany({
|
||||
where: { id, userId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async remove(userId: string, id: string) {
|
||||
return this.prisma.matter.deleteMany({
|
||||
where: { id, userId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { MedicalChronologyService } from './medical-chronology.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt.guard';
|
||||
import { SubscriptionGuard } from '../auth/subscription.guard';
|
||||
|
||||
@Controller('medical-chronology')
|
||||
@UseGuards(JwtAuthGuard, SubscriptionGuard)
|
||||
export class MedicalChronologyController {
|
||||
constructor(private readonly medicalChronologyService: MedicalChronologyService) {}
|
||||
|
||||
@Post(':matterId')
|
||||
create(@Param('matterId') matterId: string, @Body() body: { title: string }) {
|
||||
return this.medicalChronologyService.create(matterId, body.title);
|
||||
}
|
||||
|
||||
@Get(':matterId')
|
||||
findAll(@Param('matterId') matterId: string) {
|
||||
return this.medicalChronologyService.findAll(matterId);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/medical-chronology/medical-chronology.module.ts
Normal file
10
apps/api/src/medical-chronology/medical-chronology.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MedicalChronologyService } from './medical-chronology.service';
|
||||
import { MedicalChronologyController } from './medical-chronology.controller';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Module({
|
||||
controllers: [MedicalChronologyController],
|
||||
providers: [MedicalChronologyService, PrismaService],
|
||||
})
|
||||
export class MedicalChronologyModule {}
|
||||
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class MedicalChronologyService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(matterId: string, title: string) {
|
||||
return this.prisma.medicalChronology.create({
|
||||
data: { matterId, title },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(matterId: string) {
|
||||
return this.prisma.medicalChronology.findMany({ where: { matterId } });
|
||||
}
|
||||
}
|
||||
20
apps/api/src/mock-trial/mock-trial.controller.ts
Normal file
20
apps/api/src/mock-trial/mock-trial.controller.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { MockTrialService } from './mock-trial.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt.guard';
|
||||
import { SubscriptionGuard } from '../auth/subscription.guard';
|
||||
|
||||
@Controller('mock-trial')
|
||||
@UseGuards(JwtAuthGuard, SubscriptionGuard)
|
||||
export class MockTrialController {
|
||||
constructor(private readonly mockTrialService: MockTrialService) {}
|
||||
|
||||
@Post(':matterId')
|
||||
create(@Param('matterId') matterId: string, @Body() body: { title: string }) {
|
||||
return this.mockTrialService.create(matterId, body.title);
|
||||
}
|
||||
|
||||
@Get(':matterId')
|
||||
findAll(@Param('matterId') matterId: string) {
|
||||
return this.mockTrialService.findAll(matterId);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/mock-trial/mock-trial.module.ts
Normal file
10
apps/api/src/mock-trial/mock-trial.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MockTrialService } from './mock-trial.service';
|
||||
import { MockTrialController } from './mock-trial.controller';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Module({
|
||||
controllers: [MockTrialController],
|
||||
providers: [MockTrialService, PrismaService],
|
||||
})
|
||||
export class MockTrialModule {}
|
||||
17
apps/api/src/mock-trial/mock-trial.service.ts
Normal file
17
apps/api/src/mock-trial/mock-trial.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class MockTrialService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(matterId: string, title: string) {
|
||||
return this.prisma.mockTrial.create({
|
||||
data: { matterId, title },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(matterId: string) {
|
||||
return this.prisma.mockTrial.findMany({ where: { matterId } });
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,11 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
constructor(config: ConfigService) {
|
||||
super({
|
||||
datasources: {
|
||||
db: {
|
||||
url: config.get<string>('database.url'),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
139
apps/web/app/ai-assistant/page.tsx
Normal file
139
apps/web/app/ai-assistant/page.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import {
|
||||
Send,
|
||||
Paperclip,
|
||||
Bot,
|
||||
User,
|
||||
Scale,
|
||||
FileText,
|
||||
Download,
|
||||
Maximize2,
|
||||
ChevronRight,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function AiAssistantPage() {
|
||||
return (
|
||||
<div className="h-[calc(100vh-12rem)] flex gap-4 overflow-hidden -m-4">
|
||||
{/* Left Pane: Chat Interface */}
|
||||
<div className="flex-1 flex flex-col bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
|
||||
<header className="p-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-slate-50 dark:bg-slate-800/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-600 flex items-center justify-center text-white shadow-lg shadow-blue-500/20">
|
||||
<Bot size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-slate-900 dark:text-white">Legal Assistant</h3>
|
||||
<p className="text-[10px] text-green-500 font-bold uppercase tracking-wider flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></span>
|
||||
Online | GPT-4 Turbo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg text-slate-500 transition-colors">
|
||||
<Download size={18} />
|
||||
</button>
|
||||
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg text-slate-500 transition-colors">
|
||||
<Maximize2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Chat Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 flex-shrink-0">
|
||||
<Bot size={18} />
|
||||
</div>
|
||||
<div className="space-y-2 max-w-[85%]">
|
||||
<div className="p-4 bg-slate-100 dark:bg-slate-800 rounded-2xl rounded-tl-none text-slate-800 dark:text-slate-200 text-sm leading-relaxed">
|
||||
Hello! I am your AI Legal Partner. I can help you draft motions, research case law, or analyze your documents. How can I assist you with your matter today?
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-medium ml-1">AI Assistant • 12:45 PM</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 flex-row-reverse">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-800 flex items-center justify-center text-slate-600 flex-shrink-0">
|
||||
<User size={18} />
|
||||
</div>
|
||||
<div className="space-y-2 max-w-[85%] text-right">
|
||||
<div className="p-4 bg-blue-600 text-white rounded-2xl rounded-tr-none text-sm leading-relaxed text-left">
|
||||
I need to draft a Motion to Dismiss based on lack of personal jurisdiction for my North Carolina case. Can you find relevant statutes?
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-medium mr-1">You • 12:46 PM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="p-4 border-t border-slate-200 dark:border-slate-800">
|
||||
<div className="relative group">
|
||||
<textarea
|
||||
placeholder="Ask anything... (e.g., 'Analyze the uploaded complaint for inconsistencies')"
|
||||
className="w-full bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 pr-24 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 min-h-[100px] resize-none transition-all text-sm"
|
||||
/>
|
||||
<div className="absolute right-3 bottom-3 flex gap-2">
|
||||
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg text-slate-500 transition-colors">
|
||||
<Paperclip size={20} />
|
||||
</button>
|
||||
<button className="p-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all shadow-lg shadow-blue-500/20">
|
||||
<Send size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-[10px] text-slate-400 text-center uppercase tracking-widest font-semibold">
|
||||
Press Enter to send • Ctrl + Shift + U to upload
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Pane: Document Viewer Placeholder */}
|
||||
<div className="w-[450px] hidden lg:flex flex-col bg-slate-100 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
|
||||
<header className="p-4 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-blue-500" />
|
||||
<h4 className="font-bold text-sm text-slate-900 dark:text-white">Document Viewer</h4>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button className="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-md text-slate-500">
|
||||
<Search size={16} />
|
||||
</button>
|
||||
<button className="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-md text-slate-500">
|
||||
<Maximize2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-200 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
||||
<FileText size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-900 dark:text-white">No Document Selected</p>
|
||||
<p className="text-xs text-slate-500 mt-1">Select a document from the matter or upload a new one to view it side-by-side with the AI Assistant.</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 rounded-lg text-sm font-bold hover:bg-slate-50 transition-colors">
|
||||
Upload Files
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">
|
||||
<span>Current Matter Files</span>
|
||||
<ChevronRight size={14} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg cursor-pointer transition-colors group">
|
||||
<div className="w-8 h-8 rounded bg-red-50 text-red-600 flex items-center justify-center text-[10px] font-bold">PDF</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-bold text-slate-700 dark:text-slate-300 truncate">Complaint_Final.pdf</p>
|
||||
<p className="text-[10px] text-slate-400">1.2 MB • Updated 2h ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Pro Se Litigant - AI Legal Assistant",
|
||||
description: "Your AI Legal Partner for Smarter Drafting, Research, and Case Preparation.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -24,10 +25,29 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<div className="flex h-screen bg-slate-50 dark:bg-slate-950 overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<header className="h-16 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 flex items-center justify-between px-8 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-100">Welcome back, Litigant</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
|
||||
New Matter
|
||||
</button>
|
||||
<div className="w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-800 border border-slate-300 dark:border-slate-700"></div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{children}
|
||||
</div>
|
||||
<footer className="p-4 border-t border-slate-200 dark:border-slate-800 text-center text-xs text-slate-500">
|
||||
<p>Disclaimer: This service provides AI-assisted legal research and drafting tools only and does not constitute legal advice. Users are solely responsible for reviewing and validating all outputs before use. The platform is not a substitute for a licensed attorney.</p>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
89
apps/web/app/matters/page.tsx
Normal file
89
apps/web/app/matters/page.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { Plus, Search, Filter, MoreVertical, Briefcase, FileText } from 'lucide-react';
|
||||
|
||||
const matters = [
|
||||
{ id: '1', title: 'Barden v. State Farm', caseNumber: '25CV000992-620', status: 'Active', documents: 15, lastActivity: '2 hours ago' },
|
||||
{ id: '2', title: 'Malveo Estate Probate', caseNumber: 'PENDING', status: 'Researching', documents: 8, lastActivity: 'Yesterday' },
|
||||
{ id: '3', title: 'Zion Rd Property Dispute', caseNumber: '24-C-849', status: 'Drafting', documents: 22, lastActivity: 'Feb 15' },
|
||||
];
|
||||
|
||||
export default function MattersPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Legal Matters</h1>
|
||||
<p className="text-slate-500 text-sm">Organize and manage your legal cases.</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20">
|
||||
<Plus size={20} />
|
||||
Create New Matter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search matters, case numbers..."
|
||||
className="w-full pl-10 pr-4 py-2 rounded-lg border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 rounded-lg text-slate-600 dark:text-slate-300 font-medium hover:bg-slate-50 transition-colors">
|
||||
<Filter size={18} />
|
||||
Filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 dark:bg-slate-800/50 text-slate-500 text-xs uppercase tracking-wider font-bold">
|
||||
<th className="px-6 py-4">Matter Name</th>
|
||||
<th className="px-6 py-4">Case Number</th>
|
||||
<th className="px-6 py-4">Status</th>
|
||||
<th className="px-6 py-4">Documents</th>
|
||||
<th className="px-6 py-4">Last Activity</th>
|
||||
<th className="px-6 py-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{matters.map((matter) => (
|
||||
<tr key={matter.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer group">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center text-blue-600">
|
||||
<Briefcase size={16} />
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 dark:text-white group-hover:text-blue-600 transition-colors">{matter.title}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-600 dark:text-slate-400 font-mono">{matter.caseNumber}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 text-[10px] font-bold rounded-full uppercase ${
|
||||
matter.status === 'Active' ? 'bg-green-100 text-green-700' :
|
||||
matter.status === 'Researching' ? 'bg-blue-100 text-blue-700' : 'bg-orange-100 text-orange-700'
|
||||
}`}>
|
||||
{matter.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={14} className="text-slate-400" />
|
||||
{matter.documents} files
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-500">{matter.lastActivity}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="p-2 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors text-slate-400">
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
apps/web/app/mock-trial/page.tsx
Normal file
95
apps/web/app/mock-trial/page.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { Gavel, Mic, Users, MessageSquare, Play, Info } from 'lucide-react';
|
||||
|
||||
export default function MockTrialPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Mock Trial Simulator</h1>
|
||||
<p className="text-slate-500">Practice your arguments and receive real-time feedback from an AI Judge.</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-500/20 active:scale-95">
|
||||
<Play size={20} fill="currentColor" />
|
||||
Start Simulation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-slate-900 rounded-2xl aspect-video relative overflow-hidden border-4 border-slate-800 shadow-2xl">
|
||||
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1589829545856-d10d557cf95f?auto=format&fit=crop&q=80&w=2000')] bg-cover bg-center opacity-20"></div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center p-8">
|
||||
<div className="w-20 h-20 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 mb-6 border-2 border-slate-700">
|
||||
<Gavel size={40} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">The Honorable AI Judge</h2>
|
||||
<p className="text-slate-400 max-w-md mx-auto">Ready to hear your opening statement or oral argument. Click 'Start Simulation' to begin.</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-6 left-6 right-6 flex justify-between items-center">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center gap-2 bg-slate-800/80 backdrop-blur px-3 py-1.5 rounded-lg border border-slate-700">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-xs font-bold text-white uppercase tracking-widest">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-3 bg-slate-800/80 backdrop-blur rounded-full text-white hover:bg-slate-700 border border-slate-700">
|
||||
<Mic size={20} />
|
||||
</button>
|
||||
<button className="p-3 bg-slate-800/80 backdrop-blur rounded-full text-white hover:bg-slate-700 border border-slate-700">
|
||||
<Users size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-6">
|
||||
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
||||
<Info size={18} className="text-blue-500" />
|
||||
Simulation Settings
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Role</label>
|
||||
<select className="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option>Plaintiff Counsel</option>
|
||||
<option>Defendant Counsel</option>
|
||||
<option>Witness (Direct/Cross)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Judge Persona</label>
|
||||
<select className="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option>Strict & Formal</option>
|
||||
<option>Helpful & Instructive</option>
|
||||
<option>Skeptical & Challenging</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 flex flex-col h-full overflow-hidden shadow-sm">
|
||||
<header className="p-4 border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50 flex items-center gap-2">
|
||||
<MessageSquare size={18} className="text-blue-500" />
|
||||
<h3 className="font-bold text-sm">Trial Log</h3>
|
||||
</header>
|
||||
<div className="flex-1 p-4 space-y-4 min-h-[400px]">
|
||||
<div className="text-center py-10 text-slate-400">
|
||||
<p className="text-sm italic">"The court is now in session..."</p>
|
||||
<p className="text-xs mt-2">Logs will appear here during your simulation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50">
|
||||
<button className="w-full py-2 bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded-lg text-xs font-bold hover:bg-slate-300 transition-colors uppercase tracking-widest">
|
||||
Export Transcript
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,23 +1,118 @@
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Briefcase,
|
||||
MessageSquare,
|
||||
Gavel,
|
||||
Clock,
|
||||
ArrowRight,
|
||||
Plus,
|
||||
FileText
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Home() {
|
||||
const stats = [
|
||||
{ label: 'Active Matters', value: '12', icon: Briefcase, color: 'text-blue-600', bg: 'bg-blue-100' },
|
||||
{ label: 'AI Sessions', value: '45', icon: MessageSquare, color: 'text-purple-600', bg: 'bg-purple-100' },
|
||||
{ label: 'Documents', value: '128', icon: FileText, color: 'text-green-600', bg: 'bg-green-100' },
|
||||
{ label: 'Upcoming Deadlines', value: '3', icon: Clock, color: 'text-red-600', bg: 'bg-red-100' },
|
||||
];
|
||||
|
||||
const recentMatters = [
|
||||
{ id: '1', title: 'Barden v. State Farm', status: 'In Progress', date: '2 hours ago' },
|
||||
{ id: '2', title: 'Malveo Estate Probate', status: 'Researching', date: 'Yesterday' },
|
||||
{ id: '3', title: 'Zion Rd Property Dispute', status: 'Drafting', date: 'Feb 15' },
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen text-center bg-gray-50">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Pro Se Litigant Logo"
|
||||
width={180}
|
||||
height={180}
|
||||
priority
|
||||
/>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Your Legal Command Center</h1>
|
||||
<p className="text-slate-500">Overview of your ongoing cases and legal research.</p>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold mt-6">
|
||||
Pro Se Litigant
|
||||
</h1>
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="bg-white dark:bg-slate-900 p-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 font-medium">{stat.label}</p>
|
||||
<p className="text-2xl font-bold mt-1 text-slate-900 dark:text-white">{stat.value}</p>
|
||||
</div>
|
||||
<div className={`${stat.bg} p-3 rounded-lg`}>
|
||||
<stat.icon className={stat.color} size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-gray-600 max-w-xl">
|
||||
Your AI Legal Partner for Smarter Drafting, Research, and Case Preparation.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Recent Matters */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-slate-900 dark:text-white">Recent Matters</h3>
|
||||
<Link href="/matters" className="text-blue-600 hover:text-blue-700 text-sm font-medium flex items-center gap-1">
|
||||
View all <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{recentMatters.map((matter) => (
|
||||
<div key={matter.id} className="p-4 flex items-center justify-between hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:text-blue-500 transition-colors">
|
||||
<Briefcase size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-900 dark:text-white">{matter.title}</p>
|
||||
<p className="text-xs text-slate-500">{matter.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="px-3 py-1 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-xs font-bold rounded-full">
|
||||
{matter.status}
|
||||
</span>
|
||||
<ArrowRight size={18} className="text-slate-300 group-hover:text-blue-500 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900 dark:text-white">Quick Tools</h3>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<button className="flex items-center gap-4 p-4 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-all shadow-lg shadow-blue-500/20 group">
|
||||
<div className="bg-white/20 p-2 rounded-lg">
|
||||
<MessageSquare size={20} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-sm">AI Assistant</p>
|
||||
<p className="text-xs text-blue-100">Draft a motion or brief</p>
|
||||
</div>
|
||||
</button>
|
||||
<button className="flex items-center gap-4 p-4 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 hover:border-blue-500 text-slate-900 dark:text-white rounded-xl transition-all group">
|
||||
<div className="bg-slate-100 dark:bg-slate-800 p-2 rounded-lg group-hover:bg-blue-100 dark:group-hover:bg-blue-900/30 group-hover:text-blue-600 transition-colors">
|
||||
<Gavel size={20} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-sm">Mock Trial</p>
|
||||
<p className="text-xs text-slate-500">Practice your oral argument</p>
|
||||
</div>
|
||||
</button>
|
||||
<button className="flex items-center gap-4 p-4 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 hover:border-blue-500 text-slate-900 dark:text-white rounded-xl transition-all group">
|
||||
<div className="bg-slate-100 dark:bg-slate-800 p-2 rounded-lg group-hover:bg-blue-100 dark:group-hover:bg-blue-900/30 group-hover:text-blue-600 transition-colors">
|
||||
<Clock size={20} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-sm">Medical Chronology</p>
|
||||
<p className="text-xs text-slate-500">Analyze medical records</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
124
apps/web/app/subscription/page.tsx
Normal file
124
apps/web/app/subscription/page.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { Check, Shield, Star, Zap, Globe, FileSearch } from 'lucide-react';
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: 'Free',
|
||||
price: '$0',
|
||||
description: 'Perfect for getting started and basic legal help.',
|
||||
features: [
|
||||
'Basic AI Chat (GPT-3.5/4o-mini)',
|
||||
'1 Active Matter',
|
||||
'1 File Upload (Max 6MB)',
|
||||
'Basic Legal Research',
|
||||
'PDF/DOCX support only'
|
||||
],
|
||||
buttonText: 'Current Plan',
|
||||
buttonClass: 'bg-slate-100 text-slate-400 cursor-not-allowed',
|
||||
highlight: false
|
||||
},
|
||||
{
|
||||
name: 'Premium',
|
||||
price: '$15',
|
||||
period: '/year',
|
||||
description: 'Complete legal arsenal for serious pro se litigants.',
|
||||
features: [
|
||||
'Advanced AI Assistant (GPT-4 Turbo)',
|
||||
'Unlimited Matters & Folders',
|
||||
'Unlimited File Size & Batch Uploads',
|
||||
'Full Federal & State Research DB',
|
||||
'Mock Trial & Medical Chronology',
|
||||
'AI Citator & Legal Drafting Canvas',
|
||||
'All Media Formats + Transcription',
|
||||
'Priority Email Support'
|
||||
],
|
||||
buttonText: 'Upgrade to Premium',
|
||||
buttonClass: 'bg-blue-600 text-white hover:bg-blue-700 shadow-xl shadow-blue-500/20',
|
||||
highlight: true
|
||||
}
|
||||
];
|
||||
|
||||
export default function SubscriptionPage() {
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-12 py-8">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">Simple, Transparent Pricing</h1>
|
||||
<p className="text-lg text-slate-500 max-w-2xl mx-auto">Get the professional legal tools you need at a price that makes sense. Choose the plan that fits your case.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={`relative p-8 bg-white dark:bg-slate-900 rounded-3xl border-2 transition-all ${
|
||||
plan.highlight
|
||||
? 'border-blue-600 shadow-2xl scale-105 z-10'
|
||||
: 'border-slate-100 dark:border-slate-800 shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{plan.highlight && (
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 px-4 py-1 bg-blue-600 text-white text-xs font-bold rounded-full uppercase tracking-widest">
|
||||
Recommended
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white">{plan.name}</h3>
|
||||
<div className="mt-2 flex items-baseline gap-1">
|
||||
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">{plan.price}</span>
|
||||
{plan.period && <span className="text-slate-500 font-medium">{plan.period}</span>}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-slate-500 leading-relaxed">{plan.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-widest">What's included:</p>
|
||||
<ul className="space-y-3">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3 text-sm text-slate-600 dark:text-slate-400">
|
||||
<div className={`mt-0.5 rounded-full p-0.5 ${plan.highlight ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 text-slate-400'}`}>
|
||||
<Check size={14} />
|
||||
</div>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button className={`w-full py-4 rounded-xl font-bold transition-all active:scale-[0.98] ${plan.buttonClass}`}>
|
||||
{plan.buttonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 dark:bg-slate-900/50 rounded-3xl p-10 border border-slate-200 dark:border-slate-800">
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-8 text-center">Premium Capabilities</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="space-y-3 text-center md:text-left">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 mx-auto md:mx-0">
|
||||
<Shield size={20} />
|
||||
</div>
|
||||
<h4 className="font-bold text-slate-900 dark:text-white">AI Citator</h4>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">Validate case citations and detect negative treatment in real-time.</p>
|
||||
</div>
|
||||
<div className="space-y-3 text-center md:text-left">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 mx-auto md:mx-0">
|
||||
<FileSearch size={20} />
|
||||
</div>
|
||||
<h4 className="font-bold text-slate-900 dark:text-white">Legal Research</h4>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">Access a comprehensive database of Federal and State appellate opinions.</p>
|
||||
</div>
|
||||
<div className="space-y-3 text-center md:text-left">
|
||||
<div className="w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center text-orange-600 mx-auto md:mx-0">
|
||||
<Zap size={20} />
|
||||
</div>
|
||||
<h4 className="font-bold text-slate-900 dark:text-white">Transcription</h4>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">AI-powered transcription for all media formats with speaker diarization.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
221
apps/web/app/transcribe/page.tsx
Normal file
221
apps/web/app/transcribe/page.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
Search,
|
||||
Download,
|
||||
Maximize2,
|
||||
Clock,
|
||||
User,
|
||||
Volume2,
|
||||
FileText
|
||||
} from 'lucide-react';
|
||||
|
||||
// Mock transcription data for demonstration
|
||||
const mockTranscript = [
|
||||
{ id: 1, start: 0, end: 5, speaker: "Speaker 1", text: "Welcome to the evidentiary hearing for Barden versus State Farm." },
|
||||
{ id: 2, start: 5, end: 12, speaker: "Speaker 2", text: "Thank you, Your Honor. We are here today to discuss the lack of personal jurisdiction as outlined in our recent motion." },
|
||||
{ id: 3, start: 12, end: 18, speaker: "Speaker 1", text: "Proceed. Please state your primary grounds for this challenge." },
|
||||
{ id: 4, start: 18, end: 25, speaker: "Speaker 2", text: "The defendant has no minimum contacts with the state of North Carolina, as required by the long-arm statute." },
|
||||
{ id: 5, start: 25, end: 32, speaker: "Speaker 3", text: "Objection, Your Honor. The defendant has maintained an office in Charlotte for over five years." },
|
||||
{ id: 6, start: 32, end: 40, speaker: "Speaker 1", text: "Overruled. I will allow the defense to finish their opening statement before hearing your rebuttal." },
|
||||
];
|
||||
|
||||
export default function TranscribePage() {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(40);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [playbackSpeed, setPlaybackSpeed] = useState(1);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const transcriptRef = useRef<HTMLDivElement>(null);
|
||||
const activeLineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll logic
|
||||
useEffect(() => {
|
||||
if (activeLineRef.current && transcriptRef.current) {
|
||||
activeLineRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}, [currentTime]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return [h, m, s].map(v => v < 10 ? "0" + v : v).join(":");
|
||||
};
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (audioRef.current) {
|
||||
setCurrentTime(audioRef.current.currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
const seek = (time: number) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = time;
|
||||
setCurrentTime(time);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-12rem)] flex flex-col gap-6 -m-4">
|
||||
<div className="flex justify-between items-center px-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">AI Transcribe (Sync Mode)</h1>
|
||||
<p className="text-sm text-slate-500">Real-time synchronized media player and transcript.</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search transcript..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-4 py-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors">
|
||||
<Download size={18} />
|
||||
Export SRT/VTT/PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex gap-6 overflow-hidden px-4">
|
||||
{/* Left: Media Player & Controls */}
|
||||
<div className="w-1/3 flex flex-col gap-4">
|
||||
<div className="bg-slate-900 rounded-2xl p-8 flex flex-col items-center justify-center text-white aspect-video relative overflow-hidden group">
|
||||
<Volume2 size={64} className="text-blue-500/20 mb-4" />
|
||||
<p className="font-bold">Evidentiary_Hearing_022426.mp3</p>
|
||||
<p className="text-xs text-slate-500">Matter: Barden v. State Farm</p>
|
||||
|
||||
{/* Minimalist Player UI Overlay */}
|
||||
<div className="absolute inset-x-0 bottom-0 p-6 bg-gradient-to-t from-black/80 to-transparent pt-12 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-full bg-white/20 h-1.5 rounded-full mb-4 cursor-pointer relative" onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
seek((x / rect.width) * duration);
|
||||
}}>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-blue-500 rounded-full"
|
||||
style={{ width: `${(currentTime / duration) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-mono">{formatTime(currentTime)}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<SkipBack size={20} className="cursor-pointer hover:text-blue-400" onClick={() => seek(currentTime - 5)} />
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsPlaying(!isPlaying);
|
||||
// In real app: audioRef.current.play/pause
|
||||
}}
|
||||
className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center hover:bg-blue-700 transition-all"
|
||||
>
|
||||
{isPlaying ? <Pause size={20} /> : <Play size={20} className="ml-1" />}
|
||||
</button>
|
||||
<SkipForward size={20} className="cursor-pointer hover:text-blue-400" onClick={() => seek(currentTime + 5)} />
|
||||
</div>
|
||||
<span className="text-[10px] font-mono">{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl p-6 space-y-4 shadow-sm">
|
||||
<h4 className="font-bold text-sm uppercase tracking-widest text-slate-400">Settings</h4>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Playback Speed</span>
|
||||
<select
|
||||
value={playbackSpeed}
|
||||
onChange={(e) => setPlaybackSpeed(Number(e.target.value))}
|
||||
className="bg-slate-50 dark:bg-slate-800 border-none rounded-lg text-sm font-bold p-2"
|
||||
>
|
||||
<option value={0.5}>0.5x</option>
|
||||
<option value={1}>1.0x</option>
|
||||
<option value={1.5}>1.5x</option>
|
||||
<option value={2}>2.0x</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Auto-Scroll</span>
|
||||
<div className="w-10 h-5 bg-blue-600 rounded-full relative cursor-pointer">
|
||||
<div className="absolute right-1 top-1 w-3 h-3 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Synchronized Transcript */}
|
||||
<div className="flex-1 flex flex-col bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
|
||||
<header className="px-6 py-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-slate-50 dark:bg-slate-800/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-blue-500" />
|
||||
<h4 className="font-bold text-sm">Transcript (Sync Active)</h4>
|
||||
</div>
|
||||
<div className="flex gap-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
<span className="flex items-center gap-1"><User size={12} /> 3 Speakers Identified</span>
|
||||
<span className="flex items-center gap-1"><Clock size={12} /> 00:40 Total Duration</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
ref={transcriptRef}
|
||||
className="flex-1 overflow-y-auto p-8 space-y-8 scroll-smooth"
|
||||
>
|
||||
{mockTranscript.map((line) => {
|
||||
const isActive = currentTime >= line.start && currentTime < line.end;
|
||||
const isMatch = searchQuery && line.text.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
return (
|
||||
<div
|
||||
key={line.id}
|
||||
ref={isActive ? activeLineRef : null}
|
||||
className={`flex gap-6 transition-all duration-300 rounded-xl p-4 ${
|
||||
isActive ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-100 dark:ring-blue-800 shadow-sm' : ''
|
||||
} ${isMatch ? 'bg-yellow-50 dark:bg-yellow-900/20' : ''}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => seek(line.start)}
|
||||
className={`text-[11px] font-mono font-bold w-20 flex-shrink-0 transition-colors ${
|
||||
isActive ? 'text-blue-600' : 'text-slate-400 hover:text-blue-500'
|
||||
}`}
|
||||
>
|
||||
[{formatTime(line.start)}]
|
||||
</button>
|
||||
<div className="space-y-1 flex-1">
|
||||
<p className={`text-[10px] font-bold uppercase tracking-wider ${
|
||||
isActive ? 'text-blue-600' : 'text-slate-400'
|
||||
}`}>
|
||||
{line.speaker}
|
||||
</p>
|
||||
<p className={`text-sm leading-relaxed transition-colors ${
|
||||
isActive ? 'text-slate-900 dark:text-white font-medium' : 'text-slate-600 dark:text-slate-400'
|
||||
}`}>
|
||||
{line.text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<footer className="p-4 bg-slate-50 dark:bg-slate-800/50 border-t border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
<p>Sync Latency: <100ms</p>
|
||||
<p>Autosave Active: 12:45 PM</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
apps/web/components/Sidebar.tsx
Normal file
80
apps/web/components/Sidebar.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Briefcase,
|
||||
MessageSquare,
|
||||
Search,
|
||||
Gavel,
|
||||
FileText,
|
||||
History,
|
||||
Settings,
|
||||
Shield,
|
||||
CreditCard
|
||||
} from 'lucide-react';
|
||||
|
||||
const menuItems = [
|
||||
{ icon: LayoutDashboard, label: 'Dashboard', href: '/' },
|
||||
{ icon: Briefcase, label: 'Matters', href: '/matters' },
|
||||
{ icon: MessageSquare, label: 'AI Assistant', href: '/ai-assistant' },
|
||||
{ icon: Search, label: 'Legal Research', href: '/research' },
|
||||
{ icon: Gavel, label: 'Mock Trial', href: '/mock-trial' },
|
||||
{ icon: History, label: 'Medical Chronology', href: '/medical-chronology' },
|
||||
{ icon: FileText, label: 'AI Transcribe', href: '/transcribe' },
|
||||
{ icon: FileText, label: 'Legal Drafting', href: '/drafting' },
|
||||
{ icon: Shield, label: 'AI Citator', href: '/citator' },
|
||||
];
|
||||
|
||||
const secondaryItems = [
|
||||
{ icon: CreditCard, label: 'Subscription', href: '/subscription' },
|
||||
{ icon: Settings, label: 'Settings', href: '/settings' },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="w-64 h-screen bg-slate-900 text-slate-300 flex flex-col border-r border-slate-800">
|
||||
<div className="p-6 border-b border-slate-800">
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Shield className="text-blue-500" />
|
||||
Pro Se Litigant
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-1 uppercase tracking-widest font-semibold">Legal Partner</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto p-4 space-y-1">
|
||||
{menuItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 hover:text-white transition-colors group"
|
||||
>
|
||||
<item.icon size={20} className="group-hover:text-blue-400 transition-colors" />
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-slate-800 space-y-1">
|
||||
{secondaryItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 hover:text-white transition-colors group"
|
||||
>
|
||||
<item.icon size={20} className="group-hover:text-blue-400 transition-colors" />
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-950/50">
|
||||
<div className="flex items-center gap-3 px-3 py-2 bg-blue-600/10 border border-blue-600/20 rounded-lg text-blue-400">
|
||||
<CreditCard size={18} />
|
||||
<div className="text-xs">
|
||||
<p className="font-bold">Premium Active</p>
|
||||
<Link href="/subscription" className="hover:underline">Manage Plan</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
33
apps/web/package-lock.json
generated
33
apps/web/package-lock.json
generated
@ -8,9 +8,12 @@
|
||||
"name": "pro_se_litigant",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.575.0",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@ -1331,6 +1334,15 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -2064,6 +2076,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.575.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz",
|
||||
"integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@ -2468,6 +2489,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
|
||||
@ -9,9 +9,12 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.575.0",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
23
deploy.sh
Normal file
23
deploy.sh
Normal file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deployment Script for VPS
|
||||
|
||||
# Stop script on error
|
||||
set -e
|
||||
|
||||
# Load environment variables (optional if passed via CI)
|
||||
# source .env
|
||||
|
||||
# Pull latest images
|
||||
echo "Pulling latest images..."
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
|
||||
# Restart containers
|
||||
echo "Restarting containers..."
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Prune old images to save space
|
||||
echo "Pruning old images..."
|
||||
docker image prune -f
|
||||
|
||||
echo "Deployment successful!"
|
||||
55
docker-compose.prod.yml
Normal file
55
docker-compose.prod.yml
Normal file
@ -0,0 +1,55 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: pro_se_litigant
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d pro_se_litigant"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
api:
|
||||
image: ghcr.io/${DOCKER_IMAGE_OWNER}/pro-se-litigant-api:latest
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/pro_se_litigant?schema=public
|
||||
PORT: 4000
|
||||
CORS_ORIGIN: ${CORS_ORIGIN}
|
||||
THROTTLE_TTL: 60
|
||||
THROTTLE_LIMIT: 100
|
||||
NODE_ENV: production
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
expose:
|
||||
- 4000
|
||||
|
||||
web:
|
||||
image: ghcr.io/${DOCKER_IMAGE_OWNER}/pro-se-litigant-web:latest
|
||||
restart: always
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||
NODE_ENV: production
|
||||
expose:
|
||||
- 3000
|
||||
|
||||
nginx:
|
||||
image: nginx:stable-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./infrastructure/nginx/conf.d:/etc/nginx/conf.d
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@ -39,7 +39,7 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: https://api.proselitigant.com # Replace with your domain
|
||||
NEXT_PUBLIC_API_URL: https://api.proselitigant.tech
|
||||
NODE_ENV: production
|
||||
expose:
|
||||
- 3000
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.proselitigant.com localhost;
|
||||
server_name api.proselitigant.tech localhost;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY;
|
||||
@ -17,6 +17,5 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
}
|
||||
|
||||
23
infrastructure/nginx/conf.d/default.conf
Normal file
23
infrastructure/nginx/conf.d/default.conf
Normal file
@ -0,0 +1,23 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api {
|
||||
rewrite ^/api/(.*) /$1 break;
|
||||
proxy_pass http://api:4000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name app.proselitigant.com localhost;
|
||||
server_name proselitigant.tech localhost;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
@ -17,6 +17,5 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user