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-jwt": "^4.0.1",
|
||||
"passport-microsoft": "^0.1.0",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"pg": "8.4.1",
|
||||
"pg-hstore": "2.3.4",
|
||||
"sequelize": "6.35.2",
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const app = express();
|
||||
@ -193,12 +192,10 @@ app.use('/api/audit_events', passport.authenticate('jwt', {session: false}), aud
|
||||
|
||||
app.use(
|
||||
'/api/openai',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
openaiRoutes,
|
||||
);
|
||||
app.use(
|
||||
'/api/ai',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
openaiRoutes,
|
||||
);
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const File_extractionsService = require('../services/file_extractions');
|
||||
@ -93,6 +92,12 @@ router.post('/', wrapAsync(async (req, res) => {
|
||||
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
|
||||
* /api/budgets/bulk-import:
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const Mock_trialsService = require('../services/mock_trials');
|
||||
@ -17,6 +16,32 @@ const {
|
||||
|
||||
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
|
||||
|
||||
@ -5,6 +5,9 @@ const router = express.Router();
|
||||
const sjs = require('sequelize-json-schema');
|
||||
const { getWidget, askGpt } = require('../services/openai');
|
||||
const { LocalAIApi } = require('../ai/LocalAIApi');
|
||||
const passport = require('passport');
|
||||
|
||||
const authMiddleware = passport.authenticate('jwt', { session: false });
|
||||
|
||||
const loadRolesModules = () => {
|
||||
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(
|
||||
'/roles-info/:infoId',
|
||||
authMiddleware,
|
||||
wrapAsync(async (req, res) => {
|
||||
const { RolesService } = loadRolesModules();
|
||||
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(
|
||||
'/info-by-key',
|
||||
authMiddleware,
|
||||
wrapAsync(async (req, res) => {
|
||||
const { RolesService, RolesDBApi } = loadRolesModules();
|
||||
const roleId = req.query.roleId;
|
||||
@ -170,6 +83,7 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/create_widget',
|
||||
authMiddleware,
|
||||
wrapAsync(async (req, res) => {
|
||||
const { RolesService } = loadRolesModules();
|
||||
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(
|
||||
'/response',
|
||||
authMiddleware,
|
||||
wrapAsync(async (req, res) => {
|
||||
const body = req.body || {};
|
||||
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(
|
||||
'/ask-gpt',
|
||||
wrapAsync(async (req, res) => {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const Research_queriesService = require('../services/research_queries');
|
||||
@ -17,6 +16,55 @@ const {
|
||||
|
||||
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
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
const db = require('../db/models');
|
||||
const File_extractionsDBApi = require('../db/api/file_extractions');
|
||||
const FilesDBApi = require('../db/api/files');
|
||||
const processFile = require("../middlewares/upload");
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
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 {
|
||||
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) {
|
||||
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 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 ValidationError = require('./notifications/errors/validation');
|
||||
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) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
@ -134,5 +229,3 @@ module.exports = class Mock_trialsService {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
const db = require('../db/models');
|
||||
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 ValidationError = require('./notifications/errors/validation');
|
||||
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) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
@ -134,5 +210,3 @@ 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,7 +8,7 @@ export const localStorageStyleKey = 'style'
|
||||
|
||||
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}`
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
{
|
||||
href: '/matters/matters-list',
|
||||
label: 'Matters',
|
||||
label: 'My Cases',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
|
||||
@ -1,166 +1,252 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import * as icon from '@mdi/js';
|
||||
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 [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
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 handleAsk = () => {
|
||||
if (prompt.trim()) {
|
||||
dispatch(askGpt(prompt));
|
||||
}
|
||||
};
|
||||
|
||||
const title = 'Pro Se Litigant AI'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
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>)
|
||||
}
|
||||
};
|
||||
const features = [
|
||||
{
|
||||
title: 'Legal Drafting',
|
||||
description: 'Draft court-ready documents, motions, and contracts with AI precision.',
|
||||
icon: icon.mdiFileDocumentEditOutline,
|
||||
},
|
||||
{
|
||||
title: 'File Analysis',
|
||||
description: 'Upload depositions or pleadings for instant AI-driven insights and summaries.',
|
||||
icon: icon.mdiFileSearchOutline,
|
||||
},
|
||||
{
|
||||
title: 'Legal Research',
|
||||
description: 'Access comprehensive US State & Federal law databases with AI assistance.',
|
||||
icon: icon.mdiLibrarySearch,
|
||||
},
|
||||
{
|
||||
title: 'Mock Trial Simulator',
|
||||
description: 'Simulate courtroom scenarios with an AI judge and opposing counsel.',
|
||||
icon: icon.mdiAccountTieVoiceOutline,
|
||||
},
|
||||
{
|
||||
title: 'Matters Management',
|
||||
description: 'Organize your case work, files, and history in dedicated workspaces.',
|
||||
icon: icon.mdiBriefcaseOutline,
|
||||
},
|
||||
{
|
||||
title: 'Medical Chronology',
|
||||
description: 'Automatically generate timelines and analysis from medical records.',
|
||||
icon: icon.mdiTimelineTextOutline,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
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',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
|
||||
<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>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{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>
|
||||
{/* Navigation */}
|
||||
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
<a href="#features" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Features</a>
|
||||
<a href="#assistant" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Assistant</a>
|
||||
<a href="#pricing" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Pricing</a>
|
||||
{currentUser ? (
|
||||
<Link href="/dashboard" className="text-sm font-semibold text-blue-600 hover:text-blue-700">Dashboard</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-blue-600 transition-colors">Login</Link>
|
||||
<BaseButton href="/login" label="Get Started" color="info" className="rounded-full px-6" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</nav>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
LandingPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
@ -90,15 +90,15 @@ const MattersTablesPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Matters')}</title>
|
||||
<title>{getPageTitle('My Cases')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Matters" main>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="My Cases" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<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
|
||||
className={'mr-3'}
|
||||
|
||||
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 DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import dayjs from "dayjs";
|
||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||
import {useRouter} from "next/router";
|
||||
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 {getPageTitle} from "../../config";
|
||||
import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton";
|
||||
import SectionMain from "../../components/SectionMain";
|
||||
import CardBox from "../../components/CardBox";
|
||||
import BaseButton from "../../components/BaseButton";
|
||||
import BaseDivider from "../../components/BaseDivider";
|
||||
import {mdiChartTimelineVariant} from "@mdi/js";
|
||||
import {SwitchField} from "../../components/SwitchField";
|
||||
import FormField from "../../components/FormField";
|
||||
import {mdiAccount, mdiGavel, mdiPlay} from "@mdi/js";
|
||||
import axios from 'axios';
|
||||
import BaseIcon from "../../components/BaseIcon";
|
||||
|
||||
|
||||
const Mock_trialsView = () => {
|
||||
const MockTrialSimulator = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const { mock_trials } = useAppSelector((state) => state.mock_trials)
|
||||
|
||||
|
||||
const { id } = router.query;
|
||||
|
||||
function removeLastCharacter(str) {
|
||||
console.log(str,`str`)
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id }));
|
||||
}, [dispatch, id]);
|
||||
if (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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('View mock_trials')}</title>
|
||||
<title>{getPageTitle('Mock Trial Simulator')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View mock_trials')} main>
|
||||
<SectionTitleLineWithButton icon={mdiGavel} title={`Simulation: ${mock_trials.session_title}`} main>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Edit'
|
||||
label='Edit Settings'
|
||||
href={`/mock_trials/mock_trials-edit/?id=${id}`}
|
||||
outline
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<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 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>
|
||||
{!mock_trials?.mock_trial_roles_mock_trial?.length && <div className={'text-center py-4'}>No data</div>}
|
||||
</CardBox>
|
||||
</>
|
||||
</div>
|
||||
|
||||
|
||||
<>
|
||||
<p className={'block font-bold mb-2'}>Mock_trial_turns MockTrial</p>
|
||||
<CardBox
|
||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||
hasTable
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th>TurnType</th>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th>SpokenAt</th>
|
||||
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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) => (
|
||||
<tr key={item.id} onClick={() => router.push(`/mock_trial_turns/mock_trial_turns-view/?id=${item.id}`)}>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td data-label="turn_type">
|
||||
{ item.turn_type }
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td data-label="spoken_at">
|
||||
{ dataFormatter.dateTimeFormatter(item.spoken_at) }
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Right: Transcript */}
|
||||
<div className="lg:col-span-3">
|
||||
<CardBox className="h-[600px] flex flex-col relative">
|
||||
<div className="flex-1 overflow-y-auto space-y-4 p-4">
|
||||
{sortedTurns.map((turn: any) => {
|
||||
const role = roles.find((r: any) => r.id === turn.roleId);
|
||||
const isJudge = role?.role_name?.toLowerCase().includes('judge');
|
||||
return (
|
||||
<div key={turn.id} className={`flex ${isJudge ? 'justify-center' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] p-4 rounded-lg shadow-sm ${
|
||||
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">
|
||||
{turn.turn_type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-800 dark:text-gray-200 whitespace-pre-wrap leading-relaxed">
|
||||
{turn.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sortedTurns.length === 0 && (
|
||||
<div className="text-center text-gray-400 mt-20">
|
||||
<p>No turns yet. Start the simulation.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
color="success"
|
||||
icon={mdiPlay}
|
||||
label={generating ? "Generating..." : "Simulate Next Turn"}
|
||||
onClick={handleGenerateTurn}
|
||||
disabled={generating || roles.length === 0}
|
||||
/>
|
||||
</div>
|
||||
{!mock_trials?.mock_trial_turns_mock_trial?.length && <div className={'text-center py-4'}>No data</div>}
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Back'
|
||||
onClick={() => router.push('/mock_trials/mock_trials-list')}
|
||||
/>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Mock_trialsView.getLayout = function getLayout(page: ReactElement) {
|
||||
MockTrialSimulator.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'READ_MOCK_TRIALS'}
|
||||
|
||||
>
|
||||
<LayoutAuthenticated permission={'READ_MOCK_TRIALS'}>
|
||||
{page}
|
||||
</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 React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import CardBox from '../../components/CardBox'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
import SectionMain from '../../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||
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 FormCheckRadio from '../../components/FormCheckRadio'
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
||||
import FormFilePicker from '../../components/FormFilePicker'
|
||||
import FormImagePicker from '../../components/FormImagePicker'
|
||||
import { SwitchField } from '../../components/SwitchField'
|
||||
|
||||
import { SelectField } from '../../components/SelectField'
|
||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||
import {RichTextField} from "../../components/RichTextField";
|
||||
|
||||
import { create } from '../../stores/research_queries/research_queriesSlice'
|
||||
import { useAppDispatch } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
requested_by: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
matter: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
query_text: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
jurisdiction: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
source_scope: 'federal',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
searched_at: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
filters_json: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const Research_queriesNew = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data))
|
||||
await router.push('/research_queries/research_queries-list')
|
||||
import FormField from '../../components/FormField'
|
||||
import axios from 'axios'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
|
||||
const AdvancedLegalResearch = () => {
|
||||
const [query, setQuery] = useState('')
|
||||
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('')
|
||||
|
||||
const handleSearch = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!query) return;
|
||||
|
||||
setLoading(true);
|
||||
setResults(null);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await axios.post('/research_queries/search', {
|
||||
data: {
|
||||
query_text: query,
|
||||
jurisdiction,
|
||||
source_scope: scope
|
||||
}
|
||||
});
|
||||
setResults(response.data.results);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Search failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
<title>{getPageTitle('Advanced Legal Research')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||
{''}
|
||||
<SectionTitleLineWithButton icon={mdiScaleBalance} title="Advanced Legal Research" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
initialValues
|
||||
|
||||
}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="RequestedBy" labelFor="requested_by">
|
||||
<Field name="requested_by" id="requested_by" component={SelectField} options={[]} itemRef={'users'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<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 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>
|
||||
<FormField label="Source Scope">
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Research_queriesNew.getLayout = function getLayout(page: ReactElement) {
|
||||
AdvancedLegalResearch.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'CREATE_RESEARCH_QUERIES'}
|
||||
|
||||
>
|
||||
<LayoutAuthenticated permission={'CREATE_RESEARCH_QUERIES'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default Research_queriesNew
|
||||
export default AdvancedLegalResearch
|
||||
Loading…
x
Reference in New Issue
Block a user