Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copy this file to server/.env (or export the variables) before running the Node service.
OPENAI_API_KEY=replace-with-your-openai-api-key
PORT=5001
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
client/node_modules/
server/node_modules/
dist/
client/dist/
server/dist/
.env
server/.env
.DS_Store
125 changes: 124 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,124 @@
# beta
# Insight Nexus CRM

Insight Nexus CRM is an immersive, local-first Vite + React command center for predictive revenue intelligence.
It fuses data management, forecasting, risk analysis, AI advisory, and deep comparison workflows into one cinematic
experience powered by your private OpenAI key.

## Architecture

```
root
├── client # Vite + React + Tailwind front-end (TypeScript)
└── server # Node/Express API broker that talks to OpenAI Responses
```

The UI runs on Vite (port 5173) and proxies API calls to the Node service (port 5001 by default). All OpenAI traffic
is handled server-side so your key never touches the browser.

## Features

- **Data fabric ops** – upload CRM CSV/JSON/TXT intelligence, track freshness, and surface AI-generated coverage maps.
- **Prediction studio** – craft multi-dimensional business scenarios, attach documents, and receive structured forecasts,
risk posture, CRM plays, and AI advisory in one response.
- **Risk command** – monitor Atlas-generated risk matrices, mitigation sprints, and telemetry streams in a premium glassmorphism UI.
- **AI advisor** – chat with Atlas, the revenue copilot, for executive-ready guidance that remembers context across turns.
- **Collaboration hub** – upload two intelligence files for deep delta analysis with suggested actions, automation plays, and executive summaries.

## Prerequisites

- Node.js 18+
- npm 9+
- An OpenAI API key with access to the Responses API (e.g., `gpt-4.1-mini`)

## Setup

### Windows quick-start

If you're on Windows 11, the repository includes helper scripts:

1. Double-click `install.bat` **or** run `install.ps1` in PowerShell to verify Node 18+/npm 9+ and install dependencies for both
the API and client. The installers validate versions, skip modules that already exist, and abort with clear messaging if the
project structure cannot be found.
2. Copy `server/.env.example` to `server/.env` and set `OPENAI_API_KEY=<your key>`.
3. Launch the stack with the automation of your choice:
- `run.bat` opens two Command Prompt windows (API + client).
- `run.ps1` opens two PowerShell windows and uses the same validation flow.

> **Important:** Seeing Flask logs such as `TemplateNotFound: index.html` means an old Python copy of the project is still
> running. Close any `python app.py` consoles, then start the new Node/Vite stack with the scripts above or via the manual steps
> below.

### Manual setup

1. **Install dependencies**

```bash
cd server && npm install
cd ../client && npm install
```

2. **Configure environment variables**

```bash
cp server/.env.example server/.env
# edit server/.env and set OPENAI_API_KEY=<your key>
```

The server respects `PORT` (default `5001`). Ensure it matches the proxy target in `client/vite.config.ts` if changed.

3. **Run the Node API broker**

```bash
cd server
npm run dev
```

4. **Run the Vite client**

In a second terminal:

```bash
cd client
npm run dev
```

5. Navigate to [http://localhost:5173](http://localhost:5173) to experience the Insight Nexus CRM suite.

### Auto-runner behaviour

- `run.bat` opens two dedicated terminals named **Insight Nexus CRM - API** and **Insight Nexus CRM - Client**. Close those
windows (or press `CTRL+C` inside each) to stop the services.
- `run.ps1` mirrors the behaviour in PowerShell so you can stay in a single shell environment if preferred.
- The runner only calls `install.bat` when `node_modules` is missing, so you can re-launch instantly once dependencies are in place.
- Both scripts exit early with actionable guidance if prerequisites are not met, helping diagnose Windows environment issues quickly.

## Usage workflow

- Start in **Command Center** to review real-time telemetry and AI advisories.
- Head to **Data Management** to ingest supporting docs and monitor Atlas automations.
- Launch predictions in **Predictive Strategy**; attach up to four documents to enrich outcomes.
- Use **Risk Command** to understand threat vectors and mitigation plays.
- Chat with **AI Advisor** for curated actions, then compare intelligence files inside **Collaboration Hub** for deep delta insights.

## Testing & linting

- Front-end: `cd client && npm run lint`
- Server: `cd server && npm run lint`

Both projects are TypeScript/ESM aware. Adjust scripts or integrate additional tooling as needed.

## Security

- Never expose your OpenAI key to the browser. Keep it in `server/.env` or your preferred secret manager.
- Do not commit `.env` files. Keys listed here are placeholders only.
- Consider adding request authentication before deploying beyond localhost.

## Troubleshooting

- If you encounter `Unable to generate insights` or `Unable to compare documents`, confirm the server console is running and that
`OPENAI_API_KEY` is valid.
- Update the `model` in `server/src/index.js` to match the tier available to your account if `gpt-4.1-mini` is unavailable.
- Still seeing `Serving Flask app` in your terminal? You're launching an old Python version. Exit the Python process, delete any
stray `app` directories from previous zips, and start this Node/Vite project from the root described above.

Enjoy orchestrating a next-generation CRM experience powered by Atlas.
21 changes: 21 additions & 0 deletions client/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:react-refresh/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
};
13 changes: 13 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Insight Nexus CRM</title>
</head>
<body class="bg-canvas text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
44 changes: 44 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "insight-crm-client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint \"src/**/*.{ts,tsx}\""
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"axios": "^1.7.7",
"framer-motion": "^11.3.16",
"lucide-react": "^0.446.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.0",
"react-router-dom": "^6.26.2",
"recharts": "^2.10.4",
"zod": "^3.23.8",
"@hookform/resolvers": "^3.9.0",
"react-dropzone": "^14.2.3"
},
"devDependencies": {
"@types/node": "^22.7.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.9.0",
"@typescript-eslint/parser": "^8.9.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.11",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
"vite": "^5.4.10"
}
}
6 changes: 6 additions & 0 deletions client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
80 changes: 80 additions & 0 deletions client/src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { FormEvent, useMemo, useState } from 'react';
import { motion } from 'framer-motion';
import { Bot, Send } from 'lucide-react';
import { useChat } from '../hooks/useChat';

export const ChatPanel = () => {
const [message, setMessage] = useState('');
const { history, sendMessage, loading } = useChat();

const reversedHistory = useMemo(() => [...history].reverse(), [history]);

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!message.trim()) return;
await sendMessage(message.trim());
setMessage('');
};

return (
<div className="flex h-[30rem] flex-col rounded-3xl border border-white/5 bg-black/40">
<div className="flex items-center gap-3 border-b border-white/5 px-6 py-4">
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-accent/20 text-accent">
<Bot className="h-5 w-5" />
</span>
<div>
<p className="text-sm font-semibold text-white">Atlas Copilot</p>
<p className="text-xs text-slate-400">Strategic CRM advisor</p>
</div>
</div>
<div className="flex-1 space-y-3 overflow-y-auto p-6">
{reversedHistory.map((entry, index) => (
<motion.div
key={`${entry.role}-${index}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`flex ${entry.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[70%] rounded-2xl border px-4 py-3 text-sm leading-relaxed shadow-lg ${
entry.role === 'user'
? 'border-accent/40 bg-accent/20 text-white'
: 'border-white/10 bg-white/5 text-slate-100'
}`}
>
{entry.content}
</div>
</motion.div>
))}
{loading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-start"
>
<div className="rounded-2xl border border-white/10 bg-white/5 px-4 py-2 text-xs text-slate-300">
Atlas is synthesizing...
</div>
</motion.div>
)}
</div>
<form onSubmit={handleSubmit} className="border-t border-white/5 p-4">
<div className="flex items-center gap-3 rounded-full border border-white/10 bg-white/5 px-4 py-2">
<input
className="flex-1 bg-transparent text-sm text-white placeholder:text-slate-500 focus:outline-none"
placeholder="Request playbooks, mitigate risk, predict outcomes..."
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
<button
type="submit"
disabled={loading}
className="flex h-10 w-10 items-center justify-center rounded-full bg-accent text-white transition hover:bg-accent/80 disabled:opacity-50"
>
<Send className="h-4 w-4" />
</button>
</div>
</form>
</div>
);
};
27 changes: 27 additions & 0 deletions client/src/components/GradientCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PropsWithChildren, ReactNode } from 'react';

interface GradientCardProps extends PropsWithChildren {
title: string;
subtitle?: string;
icon?: ReactNode;
emphasis?: 'accent' | 'neon';
}

export const GradientCard = ({ title, subtitle, icon, emphasis = 'accent', children }: GradientCardProps) => (
<div
className={`gradient-border relative overflow-hidden rounded-3xl border border-white/5 bg-slate-950/60 p-7 shadow-xl shadow-black/20 backdrop-blur-2xl transition ${
emphasis === 'accent' ? 'hover:border-accent/60' : 'hover:border-neon/60'
}`}
>
<div className="relative z-10 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-xs uppercase tracking-[0.35em] text-slate-400">{subtitle}</p>
<h3 className="text-2xl font-semibold text-white">{title}</h3>
</div>
{icon && <span className="text-accent">{icon}</span>}
</div>
<div className="space-y-4 text-sm text-slate-200">{children}</div>
</div>
</div>
);
18 changes: 18 additions & 0 deletions client/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PropsWithChildren } from 'react';
import { Navigation } from './Navigation';

export const Layout = ({ children }: PropsWithChildren) => (
<div className="min-h-screen grid grid-cols-[280px_1fr] bg-canvas text-slate-100">
<aside className="relative border-r border-white/5 bg-slate-950/70 backdrop-blur-xl">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-accent/20 via-neon/20 to-transparent" />
<Navigation />
</aside>
<main className="relative overflow-y-auto">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-accent/20 to-neon/20" />
<div className="relative px-10 pb-12 pt-16">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(75,107,251,0.12),transparent_55%)]" />
<div className="relative z-10 space-y-12">{children}</div>
</div>
</main>
</div>
);
39 changes: 39 additions & 0 deletions client/src/components/MetricCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ReactNode } from 'react';
import { motion } from 'framer-motion';

interface MetricCardProps {
label: string;
value: string;
trend?: string;
icon: ReactNode;
accent?: 'primary' | 'success' | 'warning';
}

const ACCENT_MAP = {
primary: 'from-accent/60 via-accent/30 to-transparent border-accent/50',
success: 'from-emerald-500/60 via-emerald-500/20 to-transparent border-emerald-400/40',
warning: 'from-amber-500/60 via-amber-500/20 to-transparent border-amber-400/40',
};

export const MetricCard = ({ label, value, trend, icon, accent = 'primary' }: MetricCardProps) => (
<motion.div
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className={`relative overflow-hidden rounded-3xl border bg-white/5 p-6 shadow-2xl shadow-black/20 backdrop-blur-xl ${
ACCENT_MAP[accent]
}`}
>
<div className="absolute inset-0 bg-gradient-to-br from-white/10 via-transparent to-transparent" />
<div className="relative z-10 flex items-start justify-between gap-4">
<div className="flex flex-col gap-3">
<p className="text-xs uppercase tracking-[0.35em] text-slate-400">{label}</p>
<p className="text-4xl font-semibold text-white">{value}</p>
{trend && <p className="text-sm text-slate-300">{trend}</p>}
</div>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-black/40 text-white">
{icon}
</div>
</div>
</motion.div>
);
Loading