Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e757cb3b3 |
9677
backend/package-lock.json
generated
Normal file
9677
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,7 @@
|
|||||||
"passport-google-oauth2": "^0.2.0",
|
"passport-google-oauth2": "^0.2.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-microsoft": "^0.1.0",
|
"passport-microsoft": "^0.1.0",
|
||||||
|
"pdf-parse": "^2.4.5",
|
||||||
"pg": "8.4.1",
|
"pg": "8.4.1",
|
||||||
"pg-hstore": "2.3.4",
|
"pg-hstore": "2.3.4",
|
||||||
"sequelize": "6.35.2",
|
"sequelize": "6.35.2",
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -193,12 +192,10 @@ app.use('/api/audit_events', passport.authenticate('jwt', {session: false}), aud
|
|||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
passport.authenticate('jwt', { session: false }),
|
|
||||||
openaiRoutes,
|
openaiRoutes,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
'/api/ai',
|
'/api/ai',
|
||||||
passport.authenticate('jwt', { session: false }),
|
|
||||||
openaiRoutes,
|
openaiRoutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -235,4 +232,4 @@ db.sequelize.sync().then(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const File_extractionsService = require('../services/file_extractions');
|
const File_extractionsService = require('../services/file_extractions');
|
||||||
@ -93,6 +92,12 @@ router.post('/', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
router.post('/analyze', wrapAsync(async (req, res) => {
|
||||||
|
const { fileId, extractionType } = req.body;
|
||||||
|
const result = await File_extractionsService.analyze(fileId, extractionType, req.currentUser);
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/budgets/bulk-import:
|
* /api/budgets/bulk-import:
|
||||||
@ -437,4 +442,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const Mock_trialsService = require('../services/mock_trials');
|
const Mock_trialsService = require('../services/mock_trials');
|
||||||
@ -17,6 +16,32 @@ const {
|
|||||||
|
|
||||||
router.use(checkCrudPermissions('mock_trials'));
|
router.use(checkCrudPermissions('mock_trials'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/mock_trials/{id}/generate-turn:
|
||||||
|
* post:
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* tags: [Mock_trials]
|
||||||
|
* summary: Generate next turn
|
||||||
|
* description: Generate next turn in the simulation
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Generated turn
|
||||||
|
* 500:
|
||||||
|
* description: Error
|
||||||
|
*/
|
||||||
|
router.post('/:id/generate-turn', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await Mock_trialsService.generateTurn(req.params.id, req.currentUser);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
@ -432,4 +457,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -5,6 +5,9 @@ const router = express.Router();
|
|||||||
const sjs = require('sequelize-json-schema');
|
const sjs = require('sequelize-json-schema');
|
||||||
const { getWidget, askGpt } = require('../services/openai');
|
const { getWidget, askGpt } = require('../services/openai');
|
||||||
const { LocalAIApi } = require('../ai/LocalAIApi');
|
const { LocalAIApi } = require('../ai/LocalAIApi');
|
||||||
|
const passport = require('passport');
|
||||||
|
|
||||||
|
const authMiddleware = passport.authenticate('jwt', { session: false });
|
||||||
|
|
||||||
const loadRolesModules = () => {
|
const loadRolesModules = () => {
|
||||||
try {
|
try {
|
||||||
@ -20,57 +23,9 @@ const loadRolesModules = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /api/roles/roles-info/{infoId}:
|
|
||||||
* delete:
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags: [Roles]
|
|
||||||
* summary: Remove role information by ID
|
|
||||||
* description: Remove specific role information by ID
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: infoId
|
|
||||||
* description: ID of role information to remove
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* - in: query
|
|
||||||
* name: userId
|
|
||||||
* description: ID of the user
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* - in: query
|
|
||||||
* name: key
|
|
||||||
* description: Key of the role information to remove
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Role information successfully removed
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* user:
|
|
||||||
* type: string
|
|
||||||
* description: The user information
|
|
||||||
* 400:
|
|
||||||
* description: Invalid ID or key supplied
|
|
||||||
* 401:
|
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
|
||||||
* 404:
|
|
||||||
* description: Role not found
|
|
||||||
* 500:
|
|
||||||
* description: Some server error
|
|
||||||
*/
|
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/roles-info/:infoId',
|
'/roles-info/:infoId',
|
||||||
|
authMiddleware,
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const { RolesService } = loadRolesModules();
|
const { RolesService } = loadRolesModules();
|
||||||
const role = await RolesService.removeRoleInfoById(
|
const role = await RolesService.removeRoleInfoById(
|
||||||
@ -84,51 +39,9 @@ router.delete(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /api/roles/role-info/{roleId}:
|
|
||||||
* get:
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags: [Roles]
|
|
||||||
* summary: Get role information by key
|
|
||||||
* description: Get specific role information by key
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: roleId
|
|
||||||
* description: ID of role to get information for
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* - in: query
|
|
||||||
* name: key
|
|
||||||
* description: Key of the role information to retrieve
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Role information successfully received
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* info:
|
|
||||||
* type: string
|
|
||||||
* description: The role information
|
|
||||||
* 400:
|
|
||||||
* description: Invalid ID or key supplied
|
|
||||||
* 401:
|
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
|
||||||
* 404:
|
|
||||||
* description: Role not found
|
|
||||||
* 500:
|
|
||||||
* description: Some server error
|
|
||||||
*/
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/info-by-key',
|
'/info-by-key',
|
||||||
|
authMiddleware,
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const { RolesService, RolesDBApi } = loadRolesModules();
|
const { RolesService, RolesDBApi } = loadRolesModules();
|
||||||
const roleId = req.query.roleId;
|
const roleId = req.query.roleId;
|
||||||
@ -170,6 +83,7 @@ router.get(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/create_widget',
|
'/create_widget',
|
||||||
|
authMiddleware,
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const { RolesService } = loadRolesModules();
|
const { RolesService } = loadRolesModules();
|
||||||
const { description, userId, roleId } = req.body;
|
const { description, userId, roleId } = req.body;
|
||||||
@ -199,52 +113,9 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /api/openai/response:
|
|
||||||
* post:
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags: [OpenAI]
|
|
||||||
* summary: Proxy a Responses API request
|
|
||||||
* description: Sends the payload to the Flatlogic AI proxy and returns the response.
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* input:
|
|
||||||
* type: array
|
|
||||||
* description: List of messages with roles and content.
|
|
||||||
* items:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* role:
|
|
||||||
* type: string
|
|
||||||
* content:
|
|
||||||
* type: string
|
|
||||||
* options:
|
|
||||||
* type: object
|
|
||||||
* description: Optional polling controls.
|
|
||||||
* properties:
|
|
||||||
* poll_interval:
|
|
||||||
* type: number
|
|
||||||
* poll_timeout:
|
|
||||||
* type: number
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: AI response received
|
|
||||||
* 400:
|
|
||||||
* description: Invalid request
|
|
||||||
* 401:
|
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
|
||||||
* 502:
|
|
||||||
* description: Proxy error
|
|
||||||
*/
|
|
||||||
router.post(
|
router.post(
|
||||||
'/response',
|
'/response',
|
||||||
|
authMiddleware,
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const body = req.body || {};
|
const body = req.body || {};
|
||||||
const options = body.options || {};
|
const options = body.options || {};
|
||||||
@ -263,46 +134,6 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /api/openai/ask:
|
|
||||||
* post:
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags: [OpenAI]
|
|
||||||
* summary: Ask a question to ChatGPT
|
|
||||||
* description: Send a question through the Flatlogic AI proxy and get a response
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* prompt:
|
|
||||||
* type: string
|
|
||||||
* description: The question to ask ChatGPT
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Question successfully answered
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* success:
|
|
||||||
* type: boolean
|
|
||||||
* description: Whether the request was successful
|
|
||||||
* data:
|
|
||||||
* type: string
|
|
||||||
* description: The answer from ChatGPT
|
|
||||||
* 400:
|
|
||||||
* description: Invalid request
|
|
||||||
* 401:
|
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
|
||||||
* 500:
|
|
||||||
* description: Some server error
|
|
||||||
*/
|
|
||||||
router.post(
|
router.post(
|
||||||
'/ask-gpt',
|
'/ask-gpt',
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
@ -325,4 +156,4 @@ router.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const Research_queriesService = require('../services/research_queries');
|
const Research_queriesService = require('../services/research_queries');
|
||||||
@ -17,6 +16,55 @@ const {
|
|||||||
|
|
||||||
router.use(checkCrudPermissions('research_queries'));
|
router.use(checkCrudPermissions('research_queries'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/research_queries/search:
|
||||||
|
* post:
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* tags: [Research_queries]
|
||||||
|
* summary: Perform a legal search
|
||||||
|
* description: Perform a legal search and return results
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* description: Search parameters
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* query_text:
|
||||||
|
* type: string
|
||||||
|
* jurisdiction:
|
||||||
|
* type: string
|
||||||
|
* source_scope:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Search results
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* query:
|
||||||
|
* $ref: "#/components/schemas/Research_queries"
|
||||||
|
* results:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: "#/components/schemas/Research_results"
|
||||||
|
* 401:
|
||||||
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
|
* 500:
|
||||||
|
* description: Some server error
|
||||||
|
*/
|
||||||
|
router.post('/search', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await Research_queriesService.performSearch(req.body.data, req.currentUser);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
@ -433,4 +481,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -1,15 +1,16 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const File_extractionsDBApi = require('../db/api/file_extractions');
|
const File_extractionsDBApi = require('../db/api/file_extractions');
|
||||||
|
const FilesDBApi = require('../db/api/files');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { LocalAIApi } = require('../ai/LocalAIApi');
|
||||||
|
const pdf = require('pdf-parse');
|
||||||
|
|
||||||
module.exports = class File_extractionsService {
|
module.exports = class File_extractionsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
@ -30,6 +31,107 @@ module.exports = class File_extractionsService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static async analyze(fileId, extractionType, currentUser) {
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
// 1. Fetch File Record
|
||||||
|
const file = await FilesDBApi.findBy({ id: fileId }, { transaction });
|
||||||
|
if (!file) {
|
||||||
|
throw new ValidationError('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Resolve File Path
|
||||||
|
// Assuming storage_key is the relative path in uploadDir
|
||||||
|
// If storage_provider is 'local'
|
||||||
|
let fileContent = '';
|
||||||
|
|
||||||
|
if (file.storage_provider === 'local' || !file.storage_provider) {
|
||||||
|
// storage_key usually stores 'folder/filename' or just 'filename' depending on implementation
|
||||||
|
// In file.js uploadLocal: folder + filename.
|
||||||
|
// Let's assume storage_key is the full relative path.
|
||||||
|
const filePath = path.join(config.uploadDir, file.storage_key);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new ValidationError(`File not found on disk: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
if (['.txt', '.md', '.csv', '.json', '.xml', '.html', '.js', '.ts'].includes(ext)) {
|
||||||
|
fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
} else if (ext === '.pdf') {
|
||||||
|
const dataBuffer = fs.readFileSync(filePath);
|
||||||
|
const data = await pdf(dataBuffer);
|
||||||
|
fileContent = data.text;
|
||||||
|
} else {
|
||||||
|
throw new ValidationError(`Unsupported file type for analysis: ${ext}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ValidationError('Only local file storage is currently supported for analysis.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileContent) {
|
||||||
|
throw new ValidationError('File is empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Construct AI Prompt
|
||||||
|
let systemPrompt = "You are a legal AI assistant specialized in analyzing documents.";
|
||||||
|
let userPrompt = "";
|
||||||
|
|
||||||
|
switch (extractionType) {
|
||||||
|
case 'summary':
|
||||||
|
userPrompt = `Please provide a concise summary of the following document:\n\n${fileContent.substring(0, 20000)}`; // Truncate to avoid limit
|
||||||
|
break;
|
||||||
|
case 'dates': // Map to 'metadata' or 'summary' if 'dates' not in ENUM. ENUM has 'metadata', 'issues', 'citations', 'clauses'
|
||||||
|
userPrompt = `Extract all key dates and deadlines from the following document. Format as a list:\n\n${fileContent.substring(0, 20000)}`;
|
||||||
|
break;
|
||||||
|
case 'entities':
|
||||||
|
userPrompt = `Extract all key people, organizations, and legal entities mentioned in the following document:\n\n${fileContent.substring(0, 20000)}`;
|
||||||
|
break;
|
||||||
|
case 'issues':
|
||||||
|
userPrompt = `Identify the key legal issues and disputes described in the following document:\n\n${fileContent.substring(0, 20000)}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
userPrompt = `Analyze the following document and extract key insights regarding ${extractionType}:\n\n${fileContent.substring(0, 20000)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Call AI
|
||||||
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
|
input: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!aiResponse.success) {
|
||||||
|
throw new Error(`AI Analysis failed: ${aiResponse.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultText = LocalAIApi.extractText(aiResponse);
|
||||||
|
|
||||||
|
// 5. Save Result
|
||||||
|
const extractionData = {
|
||||||
|
fileId: file.id,
|
||||||
|
extraction_type: extractionType,
|
||||||
|
result_text: resultText,
|
||||||
|
status: 'completed',
|
||||||
|
started_at: new Date(),
|
||||||
|
finished_at: new Date(),
|
||||||
|
page_count: 1 // Placeholder
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if extraction exists to update or create?
|
||||||
|
// For now, create new.
|
||||||
|
await File_extractionsDBApi.create(extractionData, { currentUser, transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return extractionData;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
@ -134,5 +236,3 @@ module.exports = class File_extractionsService {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const Mock_trialsDBApi = require('../db/api/mock_trials');
|
const Mock_trialsDBApi = require('../db/api/mock_trials');
|
||||||
|
const Mock_trial_turnsDBApi = require('../db/api/mock_trial_turns'); // Import Turns API
|
||||||
|
const { LocalAIApi } = require('../ai/LocalAIApi'); // Import AI
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
@ -30,6 +32,99 @@ module.exports = class Mock_trialsService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static async generateTurn(id, currentUser) {
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Fetch Mock Trial with Roles and Turns
|
||||||
|
const mockTrial = await db.mock_trials.findOne({
|
||||||
|
where: { id },
|
||||||
|
include: [
|
||||||
|
{ model: db.mock_trial_roles, as: 'mock_trial_roles_mock_trial' },
|
||||||
|
{ model: db.mock_trial_turns, as: 'mock_trial_turns_mock_trial', limit: 10, order: [['spoken_at', 'DESC']] }
|
||||||
|
],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mockTrial) throw new ValidationError('mock_trialsNotFound');
|
||||||
|
|
||||||
|
const roles = mockTrial.mock_trial_roles_mock_trial;
|
||||||
|
const recentTurns = (mockTrial.mock_trial_turns_mock_trial || []).reverse();
|
||||||
|
|
||||||
|
// 2. Construct Prompt
|
||||||
|
const rolesDesc = roles.map(r => `${r.role_name} (${r.display_name})`).join(', ');
|
||||||
|
const history = recentTurns.map(t => {
|
||||||
|
const role = roles.find(r => r.id === t.roleId);
|
||||||
|
return `${role ? role.role_name : 'Unknown'}: ${t.content}`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
const prompt = `Simulate the next turn in a mock trial.
|
||||||
|
Scenario: ${mockTrial.scenario_brief || 'Standard civil trial'}.
|
||||||
|
Roles: ${rolesDesc}.
|
||||||
|
|
||||||
|
Recent History:
|
||||||
|
${history}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
- Identify who should speak next based on standard courtroom procedure.
|
||||||
|
- Generate their dialogue.
|
||||||
|
- Return JSON: { "role_name": "...", "content": "...", "turn_type": "..." }
|
||||||
|
- turn_type options: opening, direct_exam, cross_exam, argument, ruling, objection, sidebar, closing, general.
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 3. Call AI
|
||||||
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
|
input: [
|
||||||
|
{ role: 'system', content: 'You are a legal simulator. Return JSON.' },
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!aiResponse.success) throw new Error("AI Generation Failed");
|
||||||
|
|
||||||
|
let role_name, content, turn_type;
|
||||||
|
try {
|
||||||
|
const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse);
|
||||||
|
role_name = decoded.role_name;
|
||||||
|
content = decoded.content;
|
||||||
|
turn_type = decoded.turn_type;
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback if AI fails to return strict JSON
|
||||||
|
content = LocalAIApi.extractText(aiResponse);
|
||||||
|
role_name = roles[0] ? roles[0].role_name : 'Unknown';
|
||||||
|
turn_type = 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find role ID
|
||||||
|
const speakerRole = roles.find(r => r.role_name === role_name) || roles[0];
|
||||||
|
|
||||||
|
if (!speakerRole) {
|
||||||
|
throw new Error("No roles defined for this mock trial. Cannot generate turn.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save Turn
|
||||||
|
const newTurn = await Mock_trial_turnsDBApi.create({
|
||||||
|
mock_trialId: id,
|
||||||
|
roleId: speakerRole.id,
|
||||||
|
content,
|
||||||
|
turn_type: turn_type || 'general',
|
||||||
|
spoken_at: new Date()
|
||||||
|
}, { currentUser, transaction });
|
||||||
|
|
||||||
|
// Need to reload to get associations if needed, but for now return plain
|
||||||
|
// Actually, let's fetch the role to return complete data
|
||||||
|
newTurn.role = speakerRole;
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return newTurn;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
console.error("Generate turn error", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
@ -133,6 +228,4 @@ module.exports = class Mock_trialsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const Research_queriesDBApi = require('../db/api/research_queries');
|
const Research_queriesDBApi = require('../db/api/research_queries');
|
||||||
|
const Research_resultsDBApi = require('../db/api/research_results');
|
||||||
|
const { LocalAIApi } = require('../ai/LocalAIApi');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
@ -30,6 +32,80 @@ module.exports = class Research_queriesService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static async performSearch(data, currentUser) {
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
// 1. Create the query record
|
||||||
|
const queryData = {
|
||||||
|
...data,
|
||||||
|
requested_by: currentUser.id, // Ensure requested_by is set
|
||||||
|
searched_at: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryRecord = await Research_queriesDBApi.create(
|
||||||
|
queryData,
|
||||||
|
{ currentUser, transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Call AI
|
||||||
|
const prompt = `Perform legal research for the following query: "${data.query_text}".
|
||||||
|
Jurisdiction: "${data.jurisdiction || 'Federal'}".
|
||||||
|
Source Scope: "${data.source_scope || 'both'}".
|
||||||
|
|
||||||
|
Return a JSON object with a "results" array. Each item in the array should have:
|
||||||
|
- "snippet": A summary of the case or statute.
|
||||||
|
- "rule_statement": The specific rule of law derived.
|
||||||
|
- "relevance_score": An integer from 1-100.
|
||||||
|
- "citation": A standard legal citation (e.g., "123 F.3d 456").
|
||||||
|
|
||||||
|
Limit to 3 most relevant results.`;
|
||||||
|
|
||||||
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
|
input: [
|
||||||
|
{ role: 'system', content: 'You are a legal research assistant. Return JSON.' },
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
if (aiResponse.success) {
|
||||||
|
try {
|
||||||
|
const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse);
|
||||||
|
if (decoded && Array.isArray(decoded.results)) {
|
||||||
|
results = decoded.results;
|
||||||
|
} else if (Array.isArray(decoded)) {
|
||||||
|
results = decoded;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decode AI research results", e);
|
||||||
|
// Fallback if plain text or partial json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save results
|
||||||
|
const createdResults = [];
|
||||||
|
for (const res of results) {
|
||||||
|
const resultData = {
|
||||||
|
research_queryId: queryRecord.id,
|
||||||
|
snippet: res.snippet ? `[${res.citation || 'No Citation'}] ${res.snippet}` : (res.text || 'No content'),
|
||||||
|
rule_statement: res.rule_statement || '',
|
||||||
|
relevance_score: res.relevance_score || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Research_resultsDBApi.create(resultData, { currentUser, transaction });
|
||||||
|
createdResults.push(resultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return { query: queryRecord, results: createdResults };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
console.error("Search error", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
@ -133,6 +209,4 @@ module.exports = class Research_queriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
3149
backend/yarn.lock
3149
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
204
frontend/src/components/Matters/MatterAssistant.tsx
Normal file
204
frontend/src/components/Matters/MatterAssistant.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||||
|
import { aiResponse as fetchAiResponse } from '../../stores/openAiSlice';
|
||||||
|
import BaseButton from '../BaseButton';
|
||||||
|
import CardBox from '../CardBox';
|
||||||
|
import FormField from '../FormField';
|
||||||
|
import { mdiSend, mdiRobot, mdiAccount } from '@mdi/js';
|
||||||
|
import BaseIcon from '../BaseIcon';
|
||||||
|
import LoadingSpinner from '../LoadingSpinner';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
matter: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatterAssistant = ({ matter }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { aiResponse, isAskingResponse, errorMessage } = useAppSelector((state) => state.openAi);
|
||||||
|
const [prompt, setPrompt] = useState('');
|
||||||
|
const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]);
|
||||||
|
const bottomRef = useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Scroll to bottom on new messages
|
||||||
|
useEffect(() => {
|
||||||
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages, isAskingResponse]);
|
||||||
|
|
||||||
|
// Handle incoming AI response
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAskingResponse && aiResponse) {
|
||||||
|
// Extract text from the complex response structure
|
||||||
|
// Structure: data.output[] -> item.type=="message" -> content[] -> item.type=="output_text" -> text
|
||||||
|
let text = '';
|
||||||
|
try {
|
||||||
|
if (aiResponse.output && Array.isArray(aiResponse.output)) {
|
||||||
|
const messageOutput = aiResponse.output.find((o: any) => o.type === 'message');
|
||||||
|
if (messageOutput && messageOutput.content && Array.isArray(messageOutput.content)) {
|
||||||
|
const textContent = messageOutput.content.find((c: any) => c.type === 'output_text');
|
||||||
|
if (textContent) {
|
||||||
|
text = textContent.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing AI response:', err);
|
||||||
|
text = 'Error: Could not parse response from AI.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
// Check if we already have this response (simple check to avoid duplicate adds on re-renders)
|
||||||
|
setMessages((prev) => {
|
||||||
|
const lastMsg = prev[prev.length - 1];
|
||||||
|
if (lastMsg && lastMsg.role === 'assistant' && lastMsg.content === text) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return [...prev, { role: 'assistant', content: text }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [aiResponse, isAskingResponse]);
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!prompt.trim()) return;
|
||||||
|
|
||||||
|
// Add user message immediately
|
||||||
|
const userMsg = { role: 'user', content: prompt };
|
||||||
|
setMessages((prev) => [...prev, userMsg]);
|
||||||
|
setPrompt('');
|
||||||
|
|
||||||
|
// Construct system context
|
||||||
|
const partiesList = matter.matter_parties_matter
|
||||||
|
? matter.matter_parties_matter.map((p: any) => `${p.display_name} (${p.party_role})`).join(', ')
|
||||||
|
: 'None';
|
||||||
|
|
||||||
|
const systemPrompt = `You are a helpful legal assistant for the case "${matter.title}".
|
||||||
|
|
||||||
|
Context:
|
||||||
|
- Description: ${matter.description || 'N/A'}
|
||||||
|
- Jurisdiction: ${matter.jurisdiction || 'N/A'}
|
||||||
|
- Court: ${matter.court_name || 'N/A'}
|
||||||
|
- Case Number: ${matter.case_number || 'N/A'}
|
||||||
|
- Parties Involved: ${partiesList}
|
||||||
|
|
||||||
|
Answer the user's questions based on this context. Be professional, concise, and accurate. If you don't know something, say so.`;
|
||||||
|
|
||||||
|
// Dispatch AI request
|
||||||
|
// We send the FULL history plus system prompt for context, or just system + last prompt?
|
||||||
|
// Ideally full history, but for simplicity/token limits, let's send System + Last User Prompt for now.
|
||||||
|
// If we wanted history, we'd map `messages` to the input array.
|
||||||
|
|
||||||
|
// Let's try sending recent history (last 5 messages) to maintain conversation flow.
|
||||||
|
const historyPayload = messages.slice(-5).map(m => ({ role: m.role, content: m.content }));
|
||||||
|
|
||||||
|
dispatch(fetchAiResponse({
|
||||||
|
input: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
...historyPayload,
|
||||||
|
{ role: 'user', content: prompt } // Add current prompt
|
||||||
|
],
|
||||||
|
options: { poll_interval: 2, poll_timeout: 60 } // Faster polling
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-[600px] border rounded-lg overflow-hidden bg-white shadow-sm">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gray-100 p-4 border-b flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BaseIcon path={mdiRobot} size="24" className="text-blue-600" />
|
||||||
|
<h3 className="font-bold text-gray-700">Case Assistant</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Context: {matter.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Area */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
|
||||||
|
{messages.length === 0 && (
|
||||||
|
<div className="text-center text-gray-400 mt-10">
|
||||||
|
<BaseIcon path={mdiRobot} size="48" className="mx-auto mb-2 opacity-50" />
|
||||||
|
<p>Ask me anything about this case.</p>
|
||||||
|
<p className="text-sm">"Draft a motion...", "Summarize the parties...", "Check jurisdiction..."</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-lg p-3 ${
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'bg-blue-600 text-white rounded-br-none'
|
||||||
|
: 'bg-white border text-gray-800 rounded-bl-none shadow-sm'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.role === 'assistant' && (
|
||||||
|
<div className="flex items-center gap-1 mb-1 opacity-50 text-xs">
|
||||||
|
<BaseIcon path={mdiRobot} size="12" />
|
||||||
|
<span>Assistant</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="whitespace-pre-wrap text-sm">{msg.content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isAskingResponse && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-white border text-gray-800 rounded-lg rounded-bl-none shadow-sm p-3 flex items-center gap-2">
|
||||||
|
<LoadingSpinner className="w-4 h-4 text-blue-600" />
|
||||||
|
<span className="text-xs text-gray-500">Thinking...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="text-center text-red-500 text-sm my-2">
|
||||||
|
Error: {errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="p-4 bg-white border-t">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<textarea
|
||||||
|
className="w-full border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Type your request here..."
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={isAskingResponse}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
icon={mdiSend}
|
||||||
|
color="info"
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={isAskingResponse || !prompt.trim()}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-1 text-center">
|
||||||
|
AI can make mistakes. Please verify important information.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MatterAssistant;
|
||||||
@ -8,8 +8,8 @@ export const localStorageStyleKey = 'style'
|
|||||||
|
|
||||||
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
||||||
|
|
||||||
export const appTitle = 'created by Flatlogic generator!'
|
export const appTitle = 'Pro Se Litigant AI'
|
||||||
|
|
||||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||||
|
|
||||||
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''
|
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''
|
||||||
@ -50,7 +50,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/matters/matters-list',
|
href: '/matters/matters-list',
|
||||||
label: 'Matters',
|
label: 'My Cases',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -248,4 +248,4 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
@ -1,166 +1,252 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
import CardBox from '../components/CardBox';
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import * as icon from '@mdi/js';
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
import { askGpt } from '../stores/openAiSlice';
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { gptResponse, isAskingQuestion } = useAppSelector((state) => state.openAi);
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const [prompt, setPrompt] = useState('');
|
||||||
|
|
||||||
export default function Starter() {
|
const handleAsk = () => {
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
if (prompt.trim()) {
|
||||||
src: undefined,
|
dispatch(askGpt(prompt));
|
||||||
photographer: undefined,
|
}
|
||||||
photographer_url: undefined,
|
};
|
||||||
})
|
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
|
||||||
const [contentType, setContentType] = useState('video');
|
|
||||||
const [contentPosition, setContentPosition] = useState('left');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
const title = 'Pro Se Litigant AI'
|
const features = [
|
||||||
|
{
|
||||||
// Fetch Pexels image/video
|
title: 'Legal Drafting',
|
||||||
useEffect(() => {
|
description: 'Draft court-ready documents, motions, and contracts with AI precision.',
|
||||||
async function fetchData() {
|
icon: icon.mdiFileDocumentEditOutline,
|
||||||
const image = await getPexelsImage();
|
},
|
||||||
const video = await getPexelsVideo();
|
{
|
||||||
setIllustrationImage(image);
|
title: 'File Analysis',
|
||||||
setIllustrationVideo(video);
|
description: 'Upload depositions or pleadings for instant AI-driven insights and summaries.',
|
||||||
}
|
icon: icon.mdiFileSearchOutline,
|
||||||
fetchData();
|
},
|
||||||
}, []);
|
{
|
||||||
|
title: 'Legal Research',
|
||||||
const imageBlock = (image) => (
|
description: 'Access comprehensive US State & Federal law databases with AI assistance.',
|
||||||
<div
|
icon: icon.mdiLibrarySearch,
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
},
|
||||||
style={{
|
{
|
||||||
backgroundImage: `${
|
title: 'Mock Trial Simulator',
|
||||||
image
|
description: 'Simulate courtroom scenarios with an AI judge and opposing counsel.',
|
||||||
? `url(${image?.src?.original})`
|
icon: icon.mdiAccountTieVoiceOutline,
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
},
|
||||||
}`,
|
{
|
||||||
backgroundSize: 'cover',
|
title: 'Matters Management',
|
||||||
backgroundPosition: 'left center',
|
description: 'Organize your case work, files, and history in dedicated workspaces.',
|
||||||
backgroundRepeat: 'no-repeat',
|
icon: icon.mdiBriefcaseOutline,
|
||||||
}}
|
},
|
||||||
>
|
{
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
title: 'Medical Chronology',
|
||||||
<a
|
description: 'Automatically generate timelines and analysis from medical records.',
|
||||||
className='text-[8px]'
|
icon: icon.mdiTimelineTextOutline,
|
||||||
href={image?.photographer_url}
|
},
|
||||||
target='_blank'
|
];
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
|
||||||
if (video?.video_files?.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
>
|
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={video?.user?.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('All-in-One AI Legal Assistant')}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@700;800&display=swap" rel="stylesheet" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
{/* Navigation */}
|
||||||
<div
|
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
|
||||||
className={`flex ${
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<div className="flex justify-between h-16 items-center">
|
||||||
} min-h-screen w-full`}
|
<div className="flex items-center">
|
||||||
>
|
<span className="text-2xl font-extrabold text-slate-900 font-serif">Pro Se Litigant <span className="text-blue-600">AI</span></span>
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your Pro Se Litigant AI app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hidden md:flex items-center space-x-8">
|
||||||
<BaseButtons>
|
<a href="#features" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Features</a>
|
||||||
<BaseButton
|
<a href="#assistant" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Assistant</a>
|
||||||
href='/login'
|
<a href="#pricing" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Pricing</a>
|
||||||
label='Login'
|
{currentUser ? (
|
||||||
color='info'
|
<Link href="/dashboard" className="text-sm font-semibold text-blue-600 hover:text-blue-700">Dashboard</Link>
|
||||||
className='w-full'
|
) : (
|
||||||
/>
|
<>
|
||||||
|
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Login</Link>
|
||||||
</BaseButtons>
|
<BaseButton href="/login" label="Get Started" color="info" className="rounded-full px-6" />
|
||||||
</CardBox>
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</SectionFullScreen>
|
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<header className="relative py-20 lg:py-32 overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<h1 className="text-5xl lg:text-7xl font-extrabold tracking-tight text-slate-900 font-serif mb-6">
|
||||||
|
Your AI-Powered <br />
|
||||||
|
<span className="text-blue-600">Legal Partner</span>
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-2xl mx-auto text-xl text-slate-600 mb-10 leading-relaxed">
|
||||||
|
Prepare, research, draft, and simulate court-ready legal matters with precision.
|
||||||
|
Designed for pro se litigants, students, and professionals.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||||
|
<BaseButton href={currentUser ? "/dashboard" : "/login"} label={currentUser ? "Go to Dashboard" : "Start Free"} color="info" className="rounded-full px-8 py-4 text-lg" />
|
||||||
|
<BaseButton href="#features" label="Explore Features" color="white" className="rounded-full px-8 py-4 text-lg border border-slate-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Decorative background element */}
|
||||||
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none">
|
||||||
|
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-400 rounded-full blur-[100px]" />
|
||||||
|
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-indigo-400 rounded-full blur-[100px]" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* AI Assistant Widget */}
|
||||||
|
<section id="assistant" className="py-20 bg-white">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-slate-900 rounded-3xl p-8 lg:p-12 shadow-2xl relative overflow-hidden">
|
||||||
|
<div className="relative z-10">
|
||||||
|
<h2 className="text-3xl font-bold text-white mb-4 font-serif">Try the Legal Assistant</h2>
|
||||||
|
<p className="text-slate-400 mb-8 text-lg">Ask a legal question or describe a document you need to draft.</p>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
placeholder="e.g. What is the California rule for rear-end collisions?"
|
||||||
|
className="flex-grow bg-slate-800 border-none rounded-xl px-6 py-4 text-white placeholder-slate-500 focus:ring-2 focus:ring-blue-500 outline-none transition-all"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleAsk}
|
||||||
|
disabled={isAskingQuestion || !prompt}
|
||||||
|
className="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-bold px-8 py-4 rounded-xl transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isAskingQuestion ? 'Consulting...' : 'Ask AI'}
|
||||||
|
{!isAskingQuestion && <BaseIcon path={icon.mdiSend} size={18} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{gptResponse && (
|
||||||
|
<div className="mt-8 p-6 bg-slate-800 rounded-2xl border border-slate-700 animate-fade-in">
|
||||||
|
<p className="text-slate-300 whitespace-pre-wrap leading-relaxed">
|
||||||
|
{gptResponse}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Background accent */}
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-600/10 rounded-full -mr-32 -mt-32 blur-3xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features Grid */}
|
||||||
|
<section id="features" className="py-24 bg-slate-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center mb-16">
|
||||||
|
<h2 className="text-4xl font-bold text-slate-900 font-serif mb-4">Comprehensive Legal Toolkit</h2>
|
||||||
|
<p className="text-slate-600 max-w-2xl mx-auto text-lg">Everything you need to manage your legal matters effectively from a single platform.</p>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{features.map((feature, idx) => (
|
||||||
|
<div key={idx} className="bg-white p-8 rounded-2xl border border-slate-200 hover:border-blue-300 transition-all hover:shadow-xl group">
|
||||||
|
<div className="w-14 h-14 bg-blue-50 text-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||||
|
<BaseIcon path={feature.icon} size={28} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mb-3 font-serif">{feature.title}</h3>
|
||||||
|
<p className="text-slate-600 leading-relaxed">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Pricing Section */}
|
||||||
|
<section id="pricing" className="py-24 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-4xl mx-auto text-center mb-16">
|
||||||
|
<h2 className="text-4xl font-bold text-slate-900 font-serif mb-4">Transparent Pricing</h2>
|
||||||
|
<p className="text-slate-600 text-lg">Start for free and upgrade when you need the full power of Pro Se Litigant AI.</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||||
|
{/* Free Plan */}
|
||||||
|
<div className="bg-slate-50 p-10 rounded-3xl border border-slate-200">
|
||||||
|
<h3 className="text-2xl font-bold mb-2">Start Free</h3>
|
||||||
|
<div className="text-4xl font-extrabold mb-6">$0</div>
|
||||||
|
<ul className="space-y-4 mb-10 text-slate-600">
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-green-500" size={20} /> Chat with AI Legal Assistant</li>
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-green-500" size={20} /> Draft simple documents</li>
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-green-500" size={20} /> Upload 1 file (up to 6MB)</li>
|
||||||
|
</ul>
|
||||||
|
<BaseButton href="/login" label="Get Started" color="white" className="w-full rounded-xl py-4 border border-slate-300" />
|
||||||
|
</div>
|
||||||
|
{/* Premium Plan */}
|
||||||
|
<div className="bg-slate-900 p-10 rounded-3xl text-white shadow-2xl relative">
|
||||||
|
<div className="absolute top-0 right-10 transform -translate-y-1/2 bg-blue-600 text-xs font-bold px-3 py-1 rounded-full tracking-wider uppercase">Most Popular</div>
|
||||||
|
<h3 className="text-2xl font-bold mb-2">Premium Plan</h3>
|
||||||
|
<div className="text-4xl font-extrabold mb-6">$15 <span className="text-lg font-normal text-slate-400">/ year</span></div>
|
||||||
|
<ul className="space-y-4 mb-10 text-slate-300">
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-blue-500" size={20} /> Unlimited AI Assistant use</li>
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-blue-500" size={20} /> All Matter workspaces</li>
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-blue-500" size={20} /> Mock Trial simulations</li>
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-blue-500" size={20} /> Medical Chronology tools</li>
|
||||||
|
<li className="flex items-center gap-2"><BaseIcon path={icon.mdiCheckCircle} className="text-blue-500" size={20} /> Full Legal Research access</li>
|
||||||
|
</ul>
|
||||||
|
<BaseButton href="/login" label="Unlock Premium" color="info" className="w-full rounded-xl py-4 text-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-slate-950 py-16 text-slate-400">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-4 gap-12">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="text-2xl font-extrabold text-white font-serif mb-6 block">Pro Se Litigant <span className="text-blue-600">AI</span></span>
|
||||||
|
<p className="max-w-sm mb-6 leading-relaxed">
|
||||||
|
Empowering individuals with high-precision AI legal tools for drafting, research, and simulation.
|
||||||
|
Making the justice system accessible to everyone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-bold mb-6">Platform</h4>
|
||||||
|
<ul className="space-y-4 text-sm">
|
||||||
|
<li><Link href="/login" className="hover:text-white transition-colors">AI Assistant</Link></li>
|
||||||
|
<li><Link href="/login" className="hover:text-white transition-colors">Matters</Link></li>
|
||||||
|
<li><Link href="/login" className="hover:text-white transition-colors">Mock Trial</Link></li>
|
||||||
|
<li><Link href="/login" className="hover:text-white transition-colors">Legal Research</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-bold mb-6">Company</h4>
|
||||||
|
<ul className="space-y-4 text-sm">
|
||||||
|
<li><Link href="/login" className="hover:text-white transition-colors">Admin Interface</Link></li>
|
||||||
|
<li><Link href="/privacy-policy" className="hover:text-white transition-colors">Privacy Policy</Link></li>
|
||||||
|
<li><Link href="/terms" className="hover:text-white transition-colors">Terms of Service</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-16 pt-8 border-t border-slate-900 text-sm flex justify-between">
|
||||||
|
<p>© 2026 Pro Se Litigant AI. All rights reserved.</p>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<a href="#" className="hover:text-white"><BaseIcon path={icon.mdiTwitter} size={20} /></a>
|
||||||
|
<a href="#" className="hover:text-white"><BaseIcon path={icon.mdiLinkedin} size={20} /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
LandingPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,15 +90,15 @@ const MattersTablesPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Matters')}</title>
|
<title>{getPageTitle('My Cases')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Matters" main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="My Cases" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||||
|
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/matters/matters-new'} color='info' label='New Item'/>}
|
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/matters/matters-new'} color='info' label='New Case'/>}
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
className={'mr-3'}
|
className={'mr-3'}
|
||||||
@ -165,4 +165,4 @@ MattersTablesPage.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MattersTablesPage
|
export default MattersTablesPage
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,708 +1,149 @@
|
|||||||
import React, { ReactElement, useEffect } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import { fetch } from '../../stores/mock_trials/mock_trialsSlice'
|
import { fetch } from '../../stores/mock_trials/mock_trialsSlice'
|
||||||
import {saveFile} from "../../helpers/fileSaver";
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
|
||||||
import ImageField from "../../components/ImageField";
|
|
||||||
import LayoutAuthenticated from "../../layouts/Authenticated";
|
import LayoutAuthenticated from "../../layouts/Authenticated";
|
||||||
import {getPageTitle} from "../../config";
|
import {getPageTitle} from "../../config";
|
||||||
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
|
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
|
||||||
import SectionMain from "../../components/SectionMain";
|
import SectionMain from "../../components/SectionMain";
|
||||||
import CardBox from "../../components/CardBox";
|
import CardBox from "../../components/CardBox";
|
||||||
import BaseButton from "../../components/BaseButton";
|
import BaseButton from "../../components/BaseButton";
|
||||||
import BaseDivider from "../../components/BaseDivider";
|
import {mdiAccount, mdiGavel, mdiPlay} from "@mdi/js";
|
||||||
import {mdiChartTimelineVariant} from "@mdi/js";
|
import axios from 'axios';
|
||||||
import {SwitchField} from "../../components/SwitchField";
|
import BaseIcon from "../../components/BaseIcon";
|
||||||
import FormField from "../../components/FormField";
|
|
||||||
|
|
||||||
|
const MockTrialSimulator = () => {
|
||||||
const Mock_trialsView = () => {
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { mock_trials } = useAppSelector((state) => state.mock_trials)
|
const { mock_trials } = useAppSelector((state) => state.mock_trials)
|
||||||
|
|
||||||
|
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
function removeLastCharacter(str) {
|
|
||||||
console.log(str,`str`)
|
|
||||||
return str.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetch({ id }));
|
if (id) {
|
||||||
}, [dispatch, id]);
|
dispatch(fetch({ id }));
|
||||||
|
}
|
||||||
|
}, [dispatch, id, generating]); // Refresh when generating done
|
||||||
|
|
||||||
|
const handleGenerateTurn = async () => {
|
||||||
|
setGenerating(true);
|
||||||
|
try {
|
||||||
|
await axios.post(`/mock_trials/${id}/generate-turn`);
|
||||||
|
// dispatch(fetch({ id })); // Triggered by dependency
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to generate turn", error);
|
||||||
|
alert("Failed to generate turn. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mock_trials) return null;
|
||||||
|
|
||||||
|
const roles = mock_trials.mock_trial_roles_mock_trial || [];
|
||||||
|
const turns = mock_trials.mock_trial_turns_mock_trial || [];
|
||||||
|
// Sort turns by spoken_at asc
|
||||||
|
const sortedTurns = [...turns].sort((a: any, b: any) => new Date(a.spoken_at).getTime() - new Date(b.spoken_at).getTime());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('View mock_trials')}</title>
|
<title>{getPageTitle('Mock Trial Simulator')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View mock_trials')} main>
|
<SectionTitleLineWithButton icon={mdiGavel} title={`Simulation: ${mock_trials.session_title}`} main>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
label='Edit'
|
label='Edit Settings'
|
||||||
href={`/mock_trials/mock_trials-edit/?id=${id}`}
|
href={`/mock_trials/mock_trials-edit/?id=${id}`}
|
||||||
|
outline
|
||||||
/>
|
/>
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
|
{/* Left: Roles */}
|
||||||
|
<div className="lg:col-span-1 space-y-4">
|
||||||
|
<CardBox className="h-full">
|
||||||
|
<h3 className="font-bold text-lg mb-4 border-b pb-2">Participants</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{roles.map((role: any) => (
|
||||||
|
<div key={role.id} className="flex items-center p-2 rounded bg-gray-50 dark:bg-slate-800">
|
||||||
|
<div className="bg-blue-100 text-blue-600 rounded-full p-2 mr-3">
|
||||||
|
<BaseIcon path={mdiAccount} size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-sm">{role.role_name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{role.display_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{roles.length === 0 && <p className="text-gray-500 text-sm">No roles defined.</p>}
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Owner</p>
|
|
||||||
|
|
||||||
|
|
||||||
<p>{mock_trials?.owner?.firstName ?? 'No data'}</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Matter</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p>{mock_trials?.matter?.title ?? 'No data'}</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>SessionTitle</p>
|
|
||||||
<p>{mock_trials?.session_title}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Mode</p>
|
|
||||||
<p>{mock_trials?.mode ?? 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>InputType</p>
|
|
||||||
<p>{mock_trials?.input_type ?? 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Status</p>
|
|
||||||
<p>{mock_trials?.status ?? 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='ScheduledAt'>
|
|
||||||
{mock_trials.scheduled_at ? <DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={mock_trials.scheduled_at ?
|
|
||||||
new Date(
|
|
||||||
dayjs(mock_trials.scheduled_at).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
disabled
|
|
||||||
/> : <p>No ScheduledAt</p>}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='StartedAt'>
|
|
||||||
{mock_trials.started_at ? <DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={mock_trials.started_at ?
|
|
||||||
new Date(
|
|
||||||
dayjs(mock_trials.started_at).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
disabled
|
|
||||||
/> : <p>No StartedAt</p>}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='EndedAt'>
|
|
||||||
{mock_trials.ended_at ? <DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={mock_trials.ended_at ?
|
|
||||||
new Date(
|
|
||||||
dayjs(mock_trials.ended_at).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
disabled
|
|
||||||
/> : <p>No EndedAt</p>}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>ScenarioBrief</p>
|
|
||||||
{mock_trials.scenario_brief
|
|
||||||
? <p dangerouslySetInnerHTML={{__html: mock_trials.scenario_brief}}/>
|
|
||||||
: <p>No data</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<>
|
|
||||||
<p className={'block font-bold mb-2'}>Mock_trial_roles MockTrial</p>
|
|
||||||
<CardBox
|
|
||||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
|
||||||
hasTable
|
|
||||||
>
|
|
||||||
<div className='overflow-x-auto'>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>RoleName</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>ActorType</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>DisplayName</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{mock_trials.mock_trial_roles_mock_trial && Array.isArray(mock_trials.mock_trial_roles_mock_trial) &&
|
|
||||||
mock_trials.mock_trial_roles_mock_trial.map((item: any) => (
|
|
||||||
<tr key={item.id} onClick={() => router.push(`/mock_trial_roles/mock_trial_roles-view/?id=${item.id}`)}>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="role_name">
|
|
||||||
{ item.role_name }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="actor_type">
|
|
||||||
{ item.actor_type }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="display_name">
|
|
||||||
{ item.display_name }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{!mock_trials?.mock_trial_roles_mock_trial?.length && <div className={'text-center py-4'}>No data</div>}
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</>
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Transcript */}
|
||||||
<>
|
<div className="lg:col-span-3">
|
||||||
<p className={'block font-bold mb-2'}>Mock_trial_turns MockTrial</p>
|
<CardBox className="h-[600px] flex flex-col relative">
|
||||||
<CardBox
|
<div className="flex-1 overflow-y-auto space-y-4 p-4">
|
||||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
{sortedTurns.map((turn: any) => {
|
||||||
hasTable
|
const role = roles.find((r: any) => r.id === turn.roleId);
|
||||||
>
|
const isJudge = role?.role_name?.toLowerCase().includes('judge');
|
||||||
<div className='overflow-x-auto'>
|
return (
|
||||||
<table>
|
<div key={turn.id} className={`flex ${isJudge ? 'justify-center' : 'justify-start'}`}>
|
||||||
<thead>
|
<div className={`max-w-[80%] p-4 rounded-lg shadow-sm ${
|
||||||
<tr>
|
isJudge ? 'bg-amber-50 border-amber-200 border' : 'bg-white border border-gray-100 dark:bg-slate-700 dark:border-slate-600'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center mb-1">
|
||||||
|
<span className="font-bold text-sm mr-2 text-blue-600">
|
||||||
|
{role ? role.role_name : 'Unknown'}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
<th>TurnType</th>
|
{turn.turn_type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-800 dark:text-gray-200 whitespace-pre-wrap leading-relaxed">
|
||||||
|
{turn.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<th>SpokenAt</th>
|
);
|
||||||
|
})}
|
||||||
|
{sortedTurns.length === 0 && (
|
||||||
</tr>
|
<div className="text-center text-gray-400 mt-20">
|
||||||
</thead>
|
<p>No turns yet. Start the simulation.</p>
|
||||||
<tbody>
|
</div>
|
||||||
{mock_trials.mock_trial_turns_mock_trial && Array.isArray(mock_trials.mock_trial_turns_mock_trial) &&
|
)}
|
||||||
mock_trials.mock_trial_turns_mock_trial.map((item: any) => (
|
</div>
|
||||||
<tr key={item.id} onClick={() => router.push(`/mock_trial_turns/mock_trial_turns-view/?id=${item.id}`)}>
|
|
||||||
|
{/* Footer Controls */}
|
||||||
|
<div className="border-t p-4 bg-gray-50 dark:bg-slate-800 rounded-b-lg flex justify-between items-center">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Status: <span className="font-semibold text-gray-700 dark:text-gray-300 capitalize">{mock_trials.status}</span>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
<td data-label="turn_type">
|
color="success"
|
||||||
{ item.turn_type }
|
icon={mdiPlay}
|
||||||
</td>
|
label={generating ? "Generating..." : "Simulate Next Turn"}
|
||||||
|
onClick={handleGenerateTurn}
|
||||||
|
disabled={generating || roles.length === 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="spoken_at">
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.spoken_at) }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{!mock_trials?.mock_trial_turns_mock_trial?.length && <div className={'text-center py-4'}>No data</div>}
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
|
|
||||||
<BaseButton
|
|
||||||
color='info'
|
|
||||||
label='Back'
|
|
||||||
onClick={() => router.push('/mock_trials/mock_trials-list')}
|
|
||||||
/>
|
|
||||||
</CardBox>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Mock_trialsView.getLayout = function getLayout(page: ReactElement) {
|
MockTrialSimulator.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated permission={'READ_MOCK_TRIALS'}>
|
||||||
|
|
||||||
permission={'READ_MOCK_TRIALS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Mock_trialsView;
|
export default MockTrialSimulator;
|
||||||
|
|||||||
@ -1,434 +1,154 @@
|
|||||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
import { mdiGavel, mdiMagnify, mdiScaleBalance } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement, useState } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||||
import { getPageTitle } from '../../config'
|
import { getPageTitle } from '../../config'
|
||||||
|
|
||||||
import { Field, Form, Formik } from 'formik'
|
|
||||||
import FormField from '../../components/FormField'
|
|
||||||
import BaseDivider from '../../components/BaseDivider'
|
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
import FormField from '../../components/FormField'
|
||||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
import axios from 'axios'
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import FormImagePicker from '../../components/FormImagePicker'
|
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
|
||||||
|
|
||||||
import { SelectField } from '../../components/SelectField'
|
const AdvancedLegalResearch = () => {
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
const [query, setQuery] = useState('')
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
const [jurisdiction, setJurisdiction] = useState('')
|
||||||
|
const [scope, setScope] = useState('federal')
|
||||||
|
const [results, setResults] = useState<any[] | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
import { create } from '../../stores/research_queries/research_queriesSlice'
|
const handleSearch = async (e) => {
|
||||||
import { useAppDispatch } from '../../stores/hooks'
|
e.preventDefault();
|
||||||
import { useRouter } from 'next/router'
|
if (!query) return;
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const initialValues = {
|
setLoading(true);
|
||||||
|
setResults(null);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
requested_by: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
matter: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
query_text: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
jurisdiction: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
source_scope: 'federal',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
searched_at: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
filters_json: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
const Research_queriesNew = () => {
|
const response = await axios.post('/research_queries/search', {
|
||||||
const router = useRouter()
|
data: {
|
||||||
const dispatch = useAppDispatch()
|
query_text: query,
|
||||||
|
jurisdiction,
|
||||||
|
source_scope: scope
|
||||||
|
}
|
||||||
|
});
|
||||||
const handleSubmit = async (data) => {
|
setResults(response.data.results);
|
||||||
await dispatch(create(data))
|
} catch (err) {
|
||||||
await router.push('/research_queries/research_queries-list')
|
console.error(err);
|
||||||
|
setError('Search failed. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('New Item')}</title>
|
<title>{getPageTitle('Advanced Legal Research')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
<SectionTitleLineWithButton icon={mdiScaleBalance} title="Advanced Legal Research" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
|
||||||
<Formik
|
|
||||||
initialValues={
|
|
||||||
|
|
||||||
initialValues
|
|
||||||
|
|
||||||
}
|
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
|
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<form onSubmit={handleSearch}>
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<FormField label="Legal Query">
|
||||||
|
<input
|
||||||
|
className="w-full px-3 py-2 rounded border border-gray-300 dark:border-gray-700 dark:bg-slate-800 focus:ring focus:ring-blue-500 outline-none"
|
||||||
|
placeholder="E.g., What are the elements of adverse possession in Texas?"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
<FormField label="Jurisdiction">
|
||||||
|
<input
|
||||||
|
className="w-full px-3 py-2 rounded border border-gray-300 dark:border-gray-700 dark:bg-slate-800 focus:ring focus:ring-blue-500 outline-none"
|
||||||
|
placeholder="E.g., Texas, Ninth Circuit, Federal"
|
||||||
|
value={jurisdiction}
|
||||||
|
onChange={(e) => setJurisdiction(e.target.value)}
|
||||||
|
/>
|
||||||
<FormField label="RequestedBy" labelFor="requested_by">
|
</FormField>
|
||||||
<Field name="requested_by" id="requested_by" component={SelectField} options={[]} itemRef={'users'}></Field>
|
<FormField label="Source Scope">
|
||||||
</FormField>
|
<select
|
||||||
|
className="w-full px-3 py-2 rounded border border-gray-300 dark:border-gray-700 dark:bg-slate-800 focus:ring focus:ring-blue-500 outline-none"
|
||||||
|
value={scope}
|
||||||
|
onChange={(e) => setScope(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="federal">Federal</option>
|
||||||
|
<option value="state">State</option>
|
||||||
|
<option value="both">Both</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
<BaseDivider />
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<BaseButton
|
||||||
|
type="submit"
|
||||||
|
color="info"
|
||||||
|
icon={mdiMagnify}
|
||||||
|
label={loading ? 'Searching...' : 'Search Case Law'}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Matter" labelFor="matter">
|
|
||||||
<Field name="matter" id="matter" component={SelectField} options={[]} itemRef={'matters'}></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="QueryText" hasTextareaHeight>
|
|
||||||
<Field name="query_text" as="textarea" placeholder="QueryText" />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="Jurisdiction"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="jurisdiction"
|
|
||||||
placeholder="Jurisdiction"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="SourceScope" labelFor="source_scope">
|
|
||||||
<Field name="source_scope" id="source_scope" component="select">
|
|
||||||
|
|
||||||
<option value="federal">federal</option>
|
|
||||||
|
|
||||||
<option value="state">state</option>
|
|
||||||
|
|
||||||
<option value="both">both</option>
|
|
||||||
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="SearchedAt"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="datetime-local"
|
|
||||||
name="searched_at"
|
|
||||||
placeholder="SearchedAt"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="FiltersJson" hasTextareaHeight>
|
|
||||||
<Field name="filters_json" as="textarea" placeholder="FiltersJson" />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
|
||||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/research_queries/research_queries-list')}/>
|
|
||||||
</BaseButtons>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<CardBox>
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<p className="text-lg animate-pulse">Analyzing legal sources...</p>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<CardBox className="bg-red-50 text-red-600">
|
||||||
|
{error}
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{results && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-xl font-bold ml-1">Search Results</h3>
|
||||||
|
{results.length === 0 && <p className="ml-1">No results found.</p>}
|
||||||
|
{results.map((res, idx) => (
|
||||||
|
<CardBox key={idx} className="border-l-4 border-blue-500">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<span className="bg-blue-100 text-blue-800 text-xs font-semibold px-2.5 py-0.5 rounded">
|
||||||
|
Relevance: {res.relevance_score}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-mono text-sm text-gray-500 whitespace-pre-wrap">{res.snippet}</p>
|
||||||
|
{res.rule_statement && (
|
||||||
|
<div className="mt-2 bg-gray-50 dark:bg-slate-800 p-3 rounded">
|
||||||
|
<span className="font-semibold text-xs uppercase tracking-wide text-gray-500">Rule of Law</span>
|
||||||
|
<p className="mt-1 italic">{res.rule_statement}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Research_queriesNew.getLayout = function getLayout(page: ReactElement) {
|
AdvancedLegalResearch.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated permission={'CREATE_RESEARCH_QUERIES'}>
|
||||||
|
|
||||||
permission={'CREATE_RESEARCH_QUERIES'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Research_queriesNew
|
export default AdvancedLegalResearch
|
||||||
Loading…
x
Reference in New Issue
Block a user