Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
9e757cb3b3 Autosave: 20260213-142000 2026-02-13 14:20:01 +00:00
19 changed files with 13156 additions and 4201 deletions

9677
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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,
);

View File

@ -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:

View File

@ -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

View File

@ -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) => {

View File

@ -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

View File

@ -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 {
};

View File

@ -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 {
};

View File

@ -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 {
};

File diff suppressed because it is too large Load Diff

View 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">&quot;Draft a motion...&quot;, &quot;Summarize the parties...&quot;, &quot;Check jurisdiction...&quot;</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;

View File

@ -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}`

View File

@ -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,

View File

@ -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 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 handleAsk = () => {
if (prompt.trim()) {
dispatch(askGpt(prompt));
}
};
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>
<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>
</nav>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
{/* 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"
/>
</BaseButtons>
</CardBox>
</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>
<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>;
};

View File

@ -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

View File

@ -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(() => {
if (id) {
dispatch(fetch({ id }));
}, [dispatch, 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 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 className={'mb-4'}>
<p className={'block font-bold mb-2'}>Matter</p>
<p>{mock_trials?.matter?.title ?? 'No data'}</p>
<div>
<p className="font-semibold text-sm">{role.role_name}</p>
<p className="text-xs text-gray-500">{role.display_name}</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>
{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>
</>
<>
<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>
</div>
{!mock_trials?.mock_trial_turns_mock_trial?.length && <div className={'text-center py-4'}>No data</div>}
</CardBox>
</>
{/* 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>
<BaseDivider />
{/* 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='info'
label='Back'
onClick={() => router.push('/mock_trials/mock_trials-list')}
color="success"
icon={mdiPlay}
label={generating ? "Generating..." : "Simulate Next Turn"}
onClick={handleGenerateTurn}
disabled={generating || roles.length === 0}
/>
</div>
</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;

View File

@ -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"
<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>
<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"
</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="FiltersJson" hasTextareaHeight>
<Field name="filters_json" as="textarea" placeholder="FiltersJson" />
<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 />
<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>
<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