spoodermon commited on
Commit
c1f8e04
·
0 Parent(s):

Initial commit

Browse files
.env.example ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Application settings
2
+ APP_ENV=development
3
+ JWT_SECRET=supersecretjwtkeypleasechangeinproduction12345
4
+ JWT_ALGORITHM=HS256
5
+ ACCESS_TOKEN_EXPIRE_MINUTES=1440
6
+
7
+ # Databases
8
+ DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gmail2
9
+ MONGODB_URI=mongodb://localhost:27017/
10
+ MONGODB_DB=gmail2
11
+
12
+ # Redis
13
+ REDIS_HOST=localhost
14
+ REDIS_PORT=6379
15
+
16
+ # Mock email provider (set to true to run offline / simulated)
17
+ MOCK_EMAIL_PROVIDER=true
18
+
19
+ # Google API Credentials (required if MOCK_EMAIL_PROVIDER=false)
20
+ GOOGLE_CLIENT_ID=your-google-client-id
21
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
22
+ GOOGLE_REDIRECT_URI=http://localhost:8000/api/auth/callback
23
+
24
+ # OpenAI API (required for AI agents)
25
+ OPENAI_API_KEY=your-openai-api-key
26
+
27
+ # Optional LangSmith Tracing
28
+ LANGCHAIN_TRACING_V2=false
29
+ LANGCHAIN_API_KEY=your-langchain-api-key
30
+ LANGCHAIN_PROJECT=agentic-email-app
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
.gitignore ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment files (Secrets)
2
+ .env
3
+ .env.local
4
+ .env.development.local
5
+ .env.test.local
6
+ .env.production.local
7
+
8
+ # Python virtual environments & packages
9
+ .venv/
10
+ venv/
11
+ env/
12
+ ENV/
13
+ python-local-packages/
14
+
15
+ # Python caches and build files
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+ *.so
20
+ .Python
21
+ build/
22
+ develop-eggs/
23
+ dist/
24
+ downloads/
25
+ eggs/
26
+ .eggs/
27
+ parts/
28
+ sdist/
29
+ var/
30
+ wheels/
31
+ *.egg-info/
32
+ .installed.cfg
33
+ *.egg
34
+
35
+ # Python testing & linting caches
36
+ .pytest_cache/
37
+ .tox/
38
+ .coverage
39
+ .coverage.*
40
+ .cache
41
+ nosetests.xml
42
+ coverage.xml
43
+ *.cover
44
+ .mypy_cache/
45
+ .ruff_cache/
46
+
47
+ # Node.js dependencies & builds (Frontend)
48
+ node_modules/
49
+ .next/
50
+ out/
51
+ frontend/node_modules/
52
+ frontend/.next/
53
+ frontend/out/
54
+ .npm
55
+ .eslintcache
56
+
57
+ # Logs and schedule data
58
+ *.log
59
+ npm-debug.log*
60
+ yarn-debug.log*
61
+ yarn-error.log*
62
+ celerybeat-schedule
63
+ celerybeat-schedule.*
64
+ dump.rdb
65
+ backend/celerybeat-schedule*
66
+
67
+ # Local development task states & compaction artifacts
68
+ .system_generated/
69
+ scratch/
70
+ Cwd/
71
+ *.bak
72
+ *.dat
73
+ *.dir
74
+
75
+ # Docker volumes and persistent state data
76
+ pgdata/
77
+ mongodata/
78
+ grafanadata/
79
+
80
+ # Operating system / IDE specific files
81
+ .DS_Store
82
+ Thumbs.db
83
+ .idea/
84
+ .vscode/
85
+ *.swp
86
+ *.swo
87
+ *.suo
docker-compose.prod.yml ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ postgres:
3
+ image: postgres:15-alpine
4
+ container_name: gmail2_postgres_prod
5
+ restart: always
6
+ environment:
7
+ POSTGRES_USER: postgres
8
+ POSTGRES_PASSWORD: postgres
9
+ POSTGRES_DB: gmail2
10
+ ports:
11
+ - "5432:5432"
12
+ volumes:
13
+ - pgdata:/var/lib/postgresql/data
14
+
15
+ mongodb:
16
+ image: mongo:6-jammy
17
+ container_name: gmail2_mongodb_prod
18
+ restart: always
19
+ ports:
20
+ - "27017:27017"
21
+ volumes:
22
+ - mongodata:/data/db
23
+
24
+ redis:
25
+ image: redis:7-alpine
26
+ container_name: gmail2_redis_prod
27
+ restart: always
28
+ ports:
29
+ - "6379:6379"
30
+
31
+ backend:
32
+ build:
33
+ context: ./backend
34
+ dockerfile: Dockerfile
35
+ container_name: gmail2_backend_prod
36
+ restart: always
37
+ ports:
38
+ - "8000:8000"
39
+ env_file:
40
+ - .env
41
+ environment:
42
+ - DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/gmail2
43
+ - MONGODB_URI=mongodb://mongodb:27017/
44
+ - REDIS_HOST=redis
45
+ - REDIS_PORT=6379
46
+ depends_on:
47
+ - postgres
48
+ - mongodb
49
+ - redis
50
+
51
+ celery_worker:
52
+ build:
53
+ context: ./backend
54
+ dockerfile: Dockerfile
55
+ container_name: gmail2_celery_worker_prod
56
+ restart: always
57
+ command: celery -A app.tasks.celery_app.celery_app worker --loglevel=info -P solo
58
+ env_file:
59
+ - .env
60
+ environment:
61
+ - DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/gmail2
62
+ - MONGODB_URI=mongodb://mongodb:27017/
63
+ - REDIS_HOST=redis
64
+ - REDIS_PORT=6379
65
+ depends_on:
66
+ - backend
67
+ - redis
68
+
69
+ celery_beat:
70
+ build:
71
+ context: ./backend
72
+ dockerfile: Dockerfile
73
+ container_name: gmail2_celery_beat_prod
74
+ restart: always
75
+ command: celery -A app.tasks.celery_app.celery_app beat --loglevel=info
76
+ env_file:
77
+ - .env
78
+ environment:
79
+ - DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/gmail2
80
+ - MONGODB_URI=mongodb://mongodb:27017/
81
+ - REDIS_HOST=redis
82
+ - REDIS_PORT=6379
83
+ depends_on:
84
+ - backend
85
+ - redis
86
+
87
+ frontend:
88
+ build:
89
+ context: ./frontend
90
+ dockerfile: Dockerfile
91
+ args:
92
+ - NEXT_PUBLIC_API_URL=http://localhost:8000
93
+ - NEXT_PUBLIC_WS_URL=ws://localhost:8000
94
+ container_name: gmail2_frontend_prod
95
+ restart: always
96
+ ports:
97
+ - "3000:3000"
98
+ depends_on:
99
+ - backend
100
+
101
+ prometheus:
102
+ image: prom/prometheus:latest
103
+ container_name: gmail2_prometheus_prod
104
+ restart: always
105
+ volumes:
106
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
107
+ ports:
108
+ - "9090:9090"
109
+ depends_on:
110
+ - backend
111
+
112
+ grafana:
113
+ image: grafana/grafana:latest
114
+ container_name: gmail2_grafana_prod
115
+ restart: always
116
+ ports:
117
+ - "3001:3000"
118
+ depends_on:
119
+ - prometheus
120
+ volumes:
121
+ - grafanadata:/var/lib/grafana
122
+
123
+ volumes:
124
+ pgdata:
125
+ mongodata:
126
+ grafanadata:
docker-compose.yml ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ postgres:
3
+ image: postgres:15-alpine
4
+ container_name: gmail2_postgres
5
+ restart: always
6
+ environment:
7
+ POSTGRES_USER: postgres
8
+ POSTGRES_PASSWORD: postgres
9
+ POSTGRES_DB: gmail2
10
+ ports:
11
+ - "5432:5432"
12
+ volumes:
13
+ - pgdata:/var/lib/postgresql/data
14
+
15
+ mongodb:
16
+ image: mongo:6-jammy
17
+ container_name: gmail2_mongodb
18
+ restart: always
19
+ ports:
20
+ - "27017:27017"
21
+ volumes:
22
+ - mongodata:/data/db
23
+
24
+ redis:
25
+ image: redis:7-alpine
26
+ container_name: gmail2_redis
27
+ restart: always
28
+ ports:
29
+ - "6379:6379"
30
+
31
+ prometheus:
32
+ image: prom/prometheus:latest
33
+ container_name: gmail2_prometheus
34
+ restart: always
35
+ volumes:
36
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
37
+ ports:
38
+ - "9090:9090"
39
+ depends_on:
40
+ - postgres
41
+
42
+ grafana:
43
+ image: grafana/grafana:latest
44
+ container_name: gmail2_grafana
45
+ restart: always
46
+ ports:
47
+ - "3001:3000"
48
+ depends_on:
49
+ - prometheus
50
+ volumes:
51
+ - grafanadata:/var/lib/grafana
52
+
53
+ volumes:
54
+ pgdata:
55
+ mongodata:
56
+ grafanadata:
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
frontend/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine AS builder
2
+ WORKDIR /app
3
+ COPY package.json package-lock.json ./
4
+ RUN npm ci
5
+ COPY . .
6
+ ENV NEXT_TELEMETRY_DISABLED 1
7
+ ARG NEXT_PUBLIC_API_URL
8
+ ARG NEXT_PUBLIC_WS_URL
9
+ ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
10
+ ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL
11
+ RUN npm run build
12
+
13
+ FROM node:18-alpine AS runner
14
+ WORKDIR /app
15
+ ENV NODE_ENV production
16
+ ENV NEXT_TELEMETRY_DISABLED 1
17
+ COPY --from=builder /app/public ./public
18
+ COPY --from=builder /app/.next ./.next
19
+ COPY --from=builder /app/node_modules ./node_modules
20
+ COPY --from=builder /app/package.json ./package.json
21
+
22
+ EXPOSE 3000
23
+ CMD ["npm", "start"]
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/app/(app)/agent/page.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Terminal, ShieldAlert, CheckCircle, XCircle, ArrowLeft, RefreshCw } from 'lucide-react';
6
+ import AgentRunTimeline from '@/components/AgentRunTimeline';
7
+ import { API_BASE_URL } from '@/app/config';
8
+
9
+ interface AgentRunListItem {
10
+ id: string;
11
+ action_type: string;
12
+ status: string;
13
+ started_at: string;
14
+ completed_at: string | null;
15
+ email_subject: string | null;
16
+ email_id: string | null;
17
+ }
18
+
19
+ export default function AgentDashboard() {
20
+ const router = useRouter();
21
+ const [runs, setRuns] = useState<AgentRunListItem[]>([]);
22
+ const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
23
+ const [isLoading, setIsLoading] = useState(false);
24
+
25
+ useEffect(() => {
26
+ fetchRuns();
27
+ }, []);
28
+
29
+ const fetchRuns = async () => {
30
+ setIsLoading(true);
31
+ const token = localStorage.getItem('token') || '';
32
+ try {
33
+ const response = await fetch(`${API_BASE_URL}/api/agent/runs`, {
34
+ headers: {
35
+ 'Authorization': `Bearer ${token}`,
36
+ },
37
+ });
38
+ if (response.ok) {
39
+ const data = await response.json();
40
+ setRuns(data);
41
+ }
42
+ } catch (err) {
43
+ console.error(err);
44
+ } finally {
45
+ setIsLoading(false);
46
+ }
47
+ };
48
+
49
+ const getStatusIcon = (status: string) => {
50
+ switch (status.toUpperCase()) {
51
+ case 'COMPLETED':
52
+ return <CheckCircle size={15} className="text-emerald-400" />;
53
+ case 'FAILED':
54
+ return <XCircle size={15} className="text-red-400" />;
55
+ default:
56
+ return <RefreshCw size={15} className="text-purple-400 animate-spin" />;
57
+ }
58
+ };
59
+
60
+ const getStatusClass = (status: string) => {
61
+ switch (status.toUpperCase()) {
62
+ case 'COMPLETED':
63
+ return 'text-emerald-400 bg-emerald-950/20 border-emerald-900/30';
64
+ case 'FAILED':
65
+ return 'text-red-400 bg-red-950/20 border-red-900/30';
66
+ default:
67
+ return 'text-purple-400 bg-purple-950/20 border-purple-900/30';
68
+ }
69
+ };
70
+
71
+ return (
72
+ <div className="min-h-screen bg-[#06060c] px-8 py-10 relative overflow-hidden text-slate-200">
73
+ {/* Background glow */}
74
+ <div className="absolute top-1/4 right-1/4 w-[400px] h-[400px] bg-purple-950/5 rounded-full blur-[120px] pointer-events-none" />
75
+
76
+ <div className="max-w-6xl mx-auto flex flex-col gap-8 relative z-10">
77
+ {/* Header */}
78
+ <div className="flex justify-between items-center">
79
+ <div className="flex items-center gap-4">
80
+ <button
81
+ onClick={() => router.push('/inbox')}
82
+ className="p-2 hover:bg-slate-900 border border-slate-900 hover:border-slate-800 rounded-xl transition text-slate-400 hover:text-white cursor-pointer"
83
+ >
84
+ <ArrowLeft size={16} />
85
+ </button>
86
+ <div>
87
+ <h1 className="text-2xl font-bold text-white tracking-wide">Agent Audit Hub</h1>
88
+ <p className="text-xs text-slate-500 mt-1">Audit historic runs and step-by-step thinking logs</p>
89
+ </div>
90
+ </div>
91
+ <button
92
+ onClick={fetchRuns}
93
+ disabled={isLoading}
94
+ className="bg-slate-950 hover:bg-slate-900 border border-slate-800 text-slate-300 hover:text-white font-medium py-2 px-4 rounded-xl text-xs flex items-center gap-2 cursor-pointer transition"
95
+ >
96
+ <RefreshCw size={14} className={isLoading ? 'animate-spin' : ''} />
97
+ <span>Refresh Audit</span>
98
+ </button>
99
+ </div>
100
+
101
+ {/* Workspace Layout */}
102
+ <div className="grid grid-cols-12 gap-8">
103
+ {/* Runs list column */}
104
+ <div className="col-span-7 flex flex-col gap-4">
105
+ <div className="glass-panel border-slate-900 rounded-2xl overflow-hidden">
106
+ <table className="w-full text-left border-collapse">
107
+ <thead>
108
+ <tr className="bg-slate-950/50 border-b border-slate-900/60 text-slate-500 text-[10px] uppercase font-bold tracking-widest">
109
+ <th className="py-4 px-6">Task Type</th>
110
+ <th className="py-4 px-6">Target Subject</th>
111
+ <th className="py-4 px-6">Status</th>
112
+ <th className="py-4 px-6">Start Time</th>
113
+ </tr>
114
+ </thead>
115
+ <tbody className="divide-y divide-slate-900/40 text-xs">
116
+ {runs.length === 0 ? (
117
+ <tr>
118
+ <td colSpan={4} className="py-12 text-center text-slate-500 italic">
119
+ No agent run history found. Try syncing some emails first.
120
+ </td>
121
+ </tr>
122
+ ) : (
123
+ runs.map((run) => {
124
+ const isSelected = selectedRunId === run.id;
125
+ return (
126
+ <tr
127
+ key={run.id}
128
+ onClick={() => setSelectedRunId(run.id)}
129
+ className={`hover:bg-slate-900/20 cursor-pointer transition ${
130
+ isSelected ? 'bg-purple-950/10' : ''
131
+ }`}
132
+ >
133
+ <td className="py-4 px-6 font-semibold text-slate-200">
134
+ {run.action_type}
135
+ </td>
136
+ <td className="py-4 px-6 text-slate-400 font-medium truncate max-w-[200px]">
137
+ {run.email_subject || 'System Trigger'}
138
+ </td>
139
+ <td className="py-4 px-6">
140
+ <span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded border text-[10px] uppercase font-bold ${getStatusClass(run.status)}`}>
141
+ {getStatusIcon(run.status)}
142
+ <span>{run.status}</span>
143
+ </span>
144
+ </td>
145
+ <td className="py-4 px-6 font-mono text-slate-500 font-medium">
146
+ {new Date(run.started_at).toLocaleTimeString()}
147
+ </td>
148
+ </tr>
149
+ );
150
+ })
151
+ )}
152
+ </tbody>
153
+ </table>
154
+ </div>
155
+ </div>
156
+
157
+ {/* Timeline Audit Logs column */}
158
+ <div className="col-span-5">
159
+ {selectedRunId ? (
160
+ <div className="glass-panel border-purple-500/10 rounded-2xl p-6 h-[550px] overflow-y-auto">
161
+ <AgentRunTimeline runId={selectedRunId} />
162
+ </div>
163
+ ) : (
164
+ <div className="glass-panel border-slate-900 rounded-2xl p-8 flex flex-col items-center justify-center text-slate-500 text-center h-[350px] gap-3">
165
+ <Terminal size={24} className="stroke-[1.5]" />
166
+ <div>
167
+ <h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Select Execution</h3>
168
+ <p className="text-[11px] leading-relaxed max-w-[200px] mx-auto">
169
+ Click an agent row on the left to review its detailed planning and API integration logs.
170
+ </p>
171
+ </div>
172
+ </div>
173
+ )}
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
frontend/app/(app)/inbox/page.tsx ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Inbox, FileText, Settings, LogOut, Terminal, Activity, AlertOctagon } from 'lucide-react';
6
+
7
+ import EmailList from '@/components/EmailList';
8
+ import EmailDetail from '@/components/EmailDetail';
9
+ import StreamingDraft from '@/components/StreamingDraft';
10
+ import AgentStatusBar from '@/components/AgentStatusBar';
11
+ import AgentRunTimeline from '@/components/AgentRunTimeline';
12
+ import { API_BASE_URL, WS_BASE_URL } from '@/app/config';
13
+
14
+ interface EmailListItem {
15
+ id: string;
16
+ subject?: string;
17
+ sender: string;
18
+ received_at: string;
19
+ category: string;
20
+ is_read: boolean;
21
+ is_starred: boolean;
22
+ thread_id?: string;
23
+ has_attachment: boolean;
24
+ }
25
+
26
+ interface AgentStatus {
27
+ status: string;
28
+ progress: number;
29
+ active: boolean;
30
+ runId?: string;
31
+ }
32
+
33
+ interface AlertToast {
34
+ id: string;
35
+ message: string;
36
+ emailId?: string;
37
+ }
38
+
39
+ export default function InboxPage() {
40
+ const router = useRouter();
41
+ const [token, setToken] = useState<string | null>(null);
42
+ const [userEmail, setUserEmail] = useState<string>('user');
43
+
44
+ // Data states
45
+ const [emails, setEmails] = useState<EmailListItem[]>([]);
46
+ const [selectedEmail, setSelectedEmail] = useState<any | null>(null);
47
+ const [selectedEmailId, setSelectedEmailId] = useState<string | null>(null);
48
+ const [activeCategory, setActiveCategory] = useState<string | null>(null);
49
+
50
+ // Status states
51
+ const [isSyncing, setIsSyncing] = useState(false);
52
+ const [emailLoading, setEmailLoading] = useState(false);
53
+ const [agentStatus, setAgentStatus] = useState<AgentStatus>({
54
+ status: 'Sleeping',
55
+ progress: 100,
56
+ active: false,
57
+ });
58
+
59
+ // Timeline audit log states
60
+ const [activeAuditRunId, setActiveAuditRunId] = useState<string | null>(null);
61
+ const [showAuditPanel, setShowAuditPanel] = useState(false);
62
+ const [alerts, setAlerts] = useState<AlertToast[]>([]);
63
+
64
+ const wsRef = useRef<WebSocket | null>(null);
65
+
66
+ // Authenticate user & load first data
67
+ useEffect(() => {
68
+ const savedToken = localStorage.getItem('token');
69
+ if (!savedToken) {
70
+ router.push('/login');
71
+ return;
72
+ }
73
+ setToken(savedToken);
74
+
75
+ const savedEmail = localStorage.getItem('email');
76
+ if (savedEmail) {
77
+ setUserEmail(savedEmail);
78
+ }
79
+ }, [router]);
80
+
81
+ // Fetch emails when category or token changes
82
+ useEffect(() => {
83
+ if (token) {
84
+ fetchEmails();
85
+ }
86
+ }, [token, activeCategory]);
87
+
88
+ // Handle WebSocket updates
89
+ useEffect(() => {
90
+ if (!token) return;
91
+
92
+ // Connect to WebSocket Server
93
+ const wsUrl = `${WS_BASE_URL}/ws?token=${encodeURIComponent(token)}`;
94
+ const ws = new WebSocket(wsUrl);
95
+ wsRef.current = ws;
96
+
97
+ ws.onopen = () => {
98
+ console.log('WebSocket connection established.');
99
+ };
100
+
101
+ ws.onmessage = (event) => {
102
+ try {
103
+ const data = JSON.parse(event.data);
104
+ console.log('WS update received:', data);
105
+
106
+ // WebSocket payload format:
107
+ // { "status": "Analyzing email...", "progress": 40, "run_id": "...", "email_id": "...", "type": "alert" }
108
+ const status = data.status || '';
109
+ const progress = typeof data.progress === 'number' ? data.progress : 100;
110
+ const runId = data.run_id;
111
+ const emailId = data.email_id;
112
+ const type = data.type;
113
+
114
+ // If it's a popup alert
115
+ if (type === 'alert') {
116
+ const newAlert: AlertToast = {
117
+ id: Math.random().toString(),
118
+ message: status,
119
+ emailId: emailId,
120
+ };
121
+ setAlerts((prev) => [newAlert, ...prev]);
122
+
123
+ // Clear alert after 8 seconds
124
+ setTimeout(() => {
125
+ setAlerts((prev) => prev.filter((a) => a.id !== newAlert.id));
126
+ }, 8000);
127
+
128
+ // Force refetch email list
129
+ fetchEmails();
130
+ return;
131
+ }
132
+
133
+ // Update progress bar
134
+ setAgentStatus({
135
+ status,
136
+ progress,
137
+ active: progress < 100,
138
+ runId,
139
+ });
140
+
141
+ if (runId) {
142
+ setActiveAuditRunId(runId);
143
+ }
144
+
145
+ // Refetch emails if analysis completed or status sync completes
146
+ if (progress === 100) {
147
+ fetchEmails();
148
+ // If the email currently opened is the one that finished sync, reload it
149
+ if (emailId && selectedEmailId === emailId) {
150
+ fetchEmailDetails(emailId);
151
+ }
152
+ }
153
+ } catch (err) {
154
+ console.error('Failed to parse WS packet:', err);
155
+ }
156
+ };
157
+
158
+ ws.onclose = () => {
159
+ console.log('WebSocket disconnected. Reconnecting in 5 seconds...');
160
+ setTimeout(() => {
161
+ // Reconnect if tab is still active
162
+ if (localStorage.getItem('token')) {
163
+ setToken(localStorage.getItem('token'));
164
+ }
165
+ }, 5000);
166
+ };
167
+
168
+ return () => {
169
+ if (wsRef.current) {
170
+ wsRef.current.close();
171
+ }
172
+ };
173
+ }, [token, selectedEmailId]);
174
+
175
+ const fetchEmails = async () => {
176
+ if (!token) return;
177
+ const url = activeCategory
178
+ ? `${API_BASE_URL}/api/emails?category=${activeCategory}`
179
+ : `${API_BASE_URL}/api/emails`;
180
+
181
+ try {
182
+ const response = await fetch(url, {
183
+ headers: {
184
+ 'Authorization': `Bearer ${token}`,
185
+ },
186
+ });
187
+ if (response.ok) {
188
+ const data = await response.json();
189
+ setEmails(data);
190
+ }
191
+ } catch (err) {
192
+ console.error('Error fetching emails:', err);
193
+ }
194
+ };
195
+
196
+ const fetchEmailDetails = async (id: string) => {
197
+ if (!token) return;
198
+ setEmailLoading(true);
199
+ try {
200
+ const response = await fetch(`${API_BASE_URL}/api/emails/${id}`, {
201
+ headers: {
202
+ 'Authorization': `Bearer ${token}`,
203
+ },
204
+ });
205
+ if (response.ok) {
206
+ const data = await response.json();
207
+ setSelectedEmail(data);
208
+
209
+ // Mark as read in local list
210
+ setEmails((prev) =>
211
+ prev.map((e) => (e.id === id ? { ...e, is_read: true } : e))
212
+ );
213
+ }
214
+ } catch (err) {
215
+ console.error('Error fetching email details:', err);
216
+ } finally {
217
+ setEmailLoading(false);
218
+ }
219
+ };
220
+
221
+ const handleSelectEmail = (id: string) => {
222
+ setSelectedEmailId(id);
223
+ fetchEmailDetails(id);
224
+ };
225
+
226
+ const handleToggleStar = async (id: string, isStarred: boolean, e: React.MouseEvent) => {
227
+ e.stopPropagation(); // Avoid selecting row
228
+ if (!token) return;
229
+ try {
230
+ const response = await fetch(`${API_BASE_URL}/api/emails/${id}/toggle-star?is_starred=${isStarred}`, {
231
+ method: 'POST',
232
+ headers: {
233
+ 'Authorization': `Bearer ${token}`,
234
+ },
235
+ });
236
+ if (response.ok) {
237
+ setEmails((prev) =>
238
+ prev.map((item) => (item.id === id ? { ...item, is_starred: isStarred } : item))
239
+ );
240
+ if (selectedEmail && selectedEmail.id === id) {
241
+ setSelectedEmail((prev: any) => ({ ...prev, is_starred: isStarred }));
242
+ }
243
+ }
244
+ } catch (err) {
245
+ console.error(err);
246
+ }
247
+ };
248
+
249
+ const handleTriggerSync = async () => {
250
+ if (!token) return;
251
+ setIsSyncing(true);
252
+ try {
253
+ const response = await fetch(`${API_BASE_URL}/api/agent/trigger-sync`, {
254
+ method: 'POST',
255
+ headers: {
256
+ 'Authorization': `Bearer ${token}`,
257
+ },
258
+ });
259
+ if (!response.ok) {
260
+ throw new Error('Failed to trigger inbox sync');
261
+ }
262
+ } catch (err) {
263
+ console.error(err);
264
+ } finally {
265
+ setIsSyncing(false);
266
+ }
267
+ };
268
+
269
+ const handleSendEmail = async (to: string, subject: string, body: string): Promise<boolean> => {
270
+ if (!token) return false;
271
+ try {
272
+ const response = await fetch(`${API_BASE_URL}/api/emails/send`, {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ 'Authorization': `Bearer ${token}`,
277
+ },
278
+ body: JSON.stringify({ to, subject, body }),
279
+ });
280
+ return response.ok;
281
+ } catch (err) {
282
+ console.error(err);
283
+ return false;
284
+ }
285
+ };
286
+
287
+ const handleLogout = () => {
288
+ localStorage.clear();
289
+ router.push('/login');
290
+ };
291
+
292
+ const toggleAuditPanel = (runId: string) => {
293
+ setActiveAuditRunId(runId);
294
+ setShowAuditPanel(true);
295
+ };
296
+
297
+ return (
298
+ <div className="min-h-screen bg-[#06060c] flex relative overflow-hidden text-slate-200">
299
+ {/* Decorative background glows */}
300
+ <div className="absolute top-0 right-1/4 w-[500px] h-[500px] bg-purple-950/10 rounded-full blur-[140px] pointer-events-none" />
301
+ <div className="absolute bottom-0 left-1/4 w-[500px] h-[500px] bg-cyan-950/5 rounded-full blur-[140px] pointer-events-none" />
302
+
303
+ {/* Floating Alerts Container */}
304
+ <div className="fixed bottom-6 right-6 z-50 flex flex-col gap-3 max-w-sm">
305
+ {alerts.map((alert) => (
306
+ <div
307
+ key={alert.id}
308
+ onClick={() => alert.emailId && handleSelectEmail(alert.emailId)}
309
+ className="glass-panel border-cyan-500/30 p-4 rounded-xl flex gap-3 shadow-xl cursor-pointer hover:border-cyan-400 hover:shadow-[0_0_15px_rgba(6,182,212,0.15)] transition-all animate-fade-in-up items-center"
310
+ >
311
+ <div className="h-8 w-8 rounded-full bg-cyan-950/50 flex items-center justify-center text-cyan-400 shrink-0">
312
+ <Activity size={16} />
313
+ </div>
314
+ <div>
315
+ <p className="text-xs font-semibold text-slate-200">{alert.message}</p>
316
+ {alert.emailId && <p className="text-[10px] text-cyan-400 mt-0.5">Click to view details</p>}
317
+ </div>
318
+ </div>
319
+ ))}
320
+ </div>
321
+
322
+ {/* Navigation Rail */}
323
+ <aside className="w-16 bg-slate-950/60 border-r border-slate-900/60 flex flex-col items-center py-6 justify-between relative z-10 shrink-0">
324
+ <div className="flex flex-col items-center gap-8">
325
+ <div className="h-10 w-10 bg-purple-950/50 border border-purple-500/25 text-purple-400 rounded-xl flex items-center justify-center font-bold text-lg shadow-[0_0_10px_rgba(147,51,234,0.1)]">
326
+ A
327
+ </div>
328
+ <nav className="flex flex-col gap-6">
329
+ <button className="h-10 w-10 bg-purple-600/15 border border-purple-500/30 text-purple-300 rounded-xl flex items-center justify-center cursor-pointer transition">
330
+ <Inbox size={18} />
331
+ </button>
332
+ <button
333
+ onClick={() => router.push('/agent')}
334
+ className="h-10 w-10 hover:bg-slate-900/60 border border-transparent text-slate-500 hover:text-slate-300 rounded-xl flex items-center justify-center cursor-pointer transition"
335
+ >
336
+ <Terminal size={18} />
337
+ </button>
338
+ <button
339
+ onClick={() => router.push('/settings')}
340
+ className="h-10 w-10 hover:bg-slate-900/60 border border-transparent text-slate-500 hover:text-slate-300 rounded-xl flex items-center justify-center cursor-pointer transition"
341
+ >
342
+ <Settings size={18} />
343
+ </button>
344
+ </nav>
345
+ </div>
346
+ <button
347
+ onClick={handleLogout}
348
+ className="h-10 w-10 hover:bg-red-950/20 border border-transparent hover:border-red-950 text-slate-500 hover:text-red-400 rounded-xl flex items-center justify-center cursor-pointer transition"
349
+ >
350
+ <LogOut size={18} />
351
+ </button>
352
+ </aside>
353
+
354
+ {/* Main App Workspace */}
355
+ <main className="flex-1 flex overflow-hidden relative z-10">
356
+ <div className="flex-1 flex flex-col min-w-0">
357
+ <header className="h-16 border-b border-slate-900/80 px-6 flex items-center justify-between shrink-0 bg-slate-950/20">
358
+ <h1 className="text-base font-bold text-white tracking-wide flex items-center gap-2">
359
+ <span>Inbox Workspace</span>
360
+ </h1>
361
+ <div className="flex items-center gap-3">
362
+ <span className="text-xs text-slate-500">Logged in:</span>
363
+ <span className="text-xs bg-slate-900 border border-slate-800 px-3 py-1 rounded-full text-slate-300 font-medium font-mono">
364
+ {userEmail}
365
+ </span>
366
+ </div>
367
+ </header>
368
+
369
+ {/* Grid Layout of the Content */}
370
+ <div className="flex-1 flex overflow-hidden p-6 gap-6">
371
+ {/* Column 1: EmailList */}
372
+ <div className="w-80 shrink-0">
373
+ <EmailList
374
+ emails={emails}
375
+ selectedId={selectedEmailId}
376
+ onSelectEmail={handleSelectEmail}
377
+ onToggleStar={handleToggleStar}
378
+ activeCategory={activeCategory}
379
+ setActiveCategory={setActiveCategory}
380
+ />
381
+ </div>
382
+
383
+ {/* Column 2: Detailed View & AI Assistant Panel */}
384
+ <div className="flex-1 flex flex-col gap-6 min-w-0">
385
+ {selectedEmail ? (
386
+ <>
387
+ <div className="flex-1 min-h-0">
388
+ <EmailDetail email={selectedEmail} isLoading={emailLoading} />
389
+ </div>
390
+ <div className="shrink-0">
391
+ <StreamingDraft
392
+ emailId={selectedEmail.id}
393
+ emailSubject={selectedEmail.subject}
394
+ emailSender={selectedEmail.sender}
395
+ onSendEmail={handleSendEmail}
396
+ />
397
+ </div>
398
+ </>
399
+ ) : (
400
+ <div className="flex-1 glass-panel border-dashed border-slate-800 rounded-2xl flex flex-col items-center justify-center text-slate-500 gap-3">
401
+ <Inbox size={32} className="stroke-[1.2]" />
402
+ <div className="text-center">
403
+ <p className="text-sm font-semibold text-slate-300">Select an email to view</p>
404
+ <p className="text-xs text-slate-500 mt-1 max-w-[240px]">
405
+ Open messages from the list to display details and access the AI draft composer.
406
+ </p>
407
+ </div>
408
+ </div>
409
+ )}
410
+ </div>
411
+
412
+ {/* Column 3: Status & Auditing Bar */}
413
+ <div className="w-80 shrink-0 flex flex-col gap-6 overflow-y-auto">
414
+ <AgentStatusBar
415
+ statusData={agentStatus}
416
+ isSyncing={isSyncing}
417
+ onTriggerSync={handleTriggerSync}
418
+ onViewLogs={toggleAuditPanel}
419
+ />
420
+
421
+ {/* Audit Timeline */}
422
+ {showAuditPanel && activeAuditRunId ? (
423
+ <div className="glass-panel border-purple-500/10 rounded-2xl p-5 relative">
424
+ <button
425
+ onClick={() => setShowAuditPanel(false)}
426
+ className="absolute top-4 right-4 text-[10px] uppercase font-bold text-slate-500 hover:text-slate-300"
427
+ >
428
+ Close
429
+ </button>
430
+ <AgentRunTimeline runId={activeAuditRunId} />
431
+ </div>
432
+ ) : (
433
+ <div className="glass-panel border-slate-900 rounded-2xl p-6 flex flex-col items-center justify-center text-slate-500 gap-3 text-center h-[280px]">
434
+ <Activity size={24} className="stroke-[1.5]" />
435
+ <div>
436
+ <h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Audit Trail</h4>
437
+ <p className="text-[11px] leading-relaxed">
438
+ Start syncing or analyze an email to inspect the AI agent thinking logs here.
439
+ </p>
440
+ </div>
441
+ </div>
442
+ )}
443
+ </div>
444
+ </div>
445
+ </div>
446
+ </main>
447
+ </div>
448
+ );
449
+ }
frontend/app/(app)/settings/page.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Settings, Shield, Sliders, ArrowLeft, Key, User, FileText, CheckCircle2 } from 'lucide-react';
6
+
7
+ export default function SettingsPage() {
8
+ const router = useRouter();
9
+ const [email, setEmail] = useState('Unknown user');
10
+ const [userId, setUserId] = useState('Unknown');
11
+
12
+ useEffect(() => {
13
+ const savedEmail = localStorage.getItem('email');
14
+ const savedUserId = localStorage.getItem('userId');
15
+ if (savedEmail) setEmail(savedEmail);
16
+ if (savedUserId) setUserId(savedUserId);
17
+ }, []);
18
+
19
+ const handleLogout = () => {
20
+ localStorage.clear();
21
+ router.push('/login');
22
+ };
23
+
24
+ return (
25
+ <div className="min-h-screen bg-[#06060c] px-8 py-10 relative overflow-hidden text-slate-200">
26
+ <div className="absolute top-1/4 left-1/4 w-[350px] h-[350px] bg-cyan-950/5 rounded-full blur-[120px] pointer-events-none" />
27
+
28
+ <div className="max-w-3xl mx-auto flex flex-col gap-8 relative z-10 animate-fade-in-up">
29
+ {/* Header */}
30
+ <div className="flex items-center gap-4">
31
+ <button
32
+ onClick={() => router.push('/inbox')}
33
+ className="p-2 hover:bg-slate-900 border border-slate-900 hover:border-slate-800 rounded-xl transition text-slate-400 hover:text-white cursor-pointer"
34
+ >
35
+ <ArrowLeft size={16} />
36
+ </button>
37
+ <div>
38
+ <h1 className="text-2xl font-bold text-white tracking-wide">System Settings</h1>
39
+ <p className="text-xs text-slate-500 mt-1">Configure credentials and review application connections</p>
40
+ </div>
41
+ </div>
42
+
43
+ {/* Content Box */}
44
+ <div className="flex flex-col gap-6">
45
+ {/* Section 1: User Profile */}
46
+ <div className="glass-panel border-slate-900 rounded-2xl p-6 flex flex-col gap-4">
47
+ <div className="flex items-center gap-2 text-purple-400 font-semibold text-xs uppercase tracking-wider">
48
+ <User size={14} />
49
+ <span>User Profile</span>
50
+ </div>
51
+ <div className="grid grid-cols-2 gap-4 text-xs">
52
+ <div>
53
+ <span className="text-slate-500 block mb-1">Authenticated Account</span>
54
+ <span className="font-semibold text-slate-200 font-mono">
55
+ {email}
56
+ </span>
57
+ </div>
58
+ <div>
59
+ <span className="text-slate-500 block mb-1">Local User ID</span>
60
+ <span className="font-semibold text-slate-300 font-mono truncate block">
61
+ {userId}
62
+ </span>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ {/* Section 2: Integration status */}
68
+ <div className="glass-panel border-slate-900 rounded-2xl p-6 flex flex-col gap-4">
69
+ <div className="flex items-center gap-2 text-cyan-400 font-semibold text-xs uppercase tracking-wider">
70
+ <Sliders size={14} />
71
+ <span>Integration Settings</span>
72
+ </div>
73
+ <div className="flex flex-col gap-3.5">
74
+ <div className="flex justify-between items-center bg-slate-950/40 p-4 border border-slate-900 rounded-xl text-xs">
75
+ <div>
76
+ <span className="font-semibold text-slate-200 block">Email Mock Mode</span>
77
+ <span className="text-slate-500 block mt-0.5">Offline mock sync handles mail simulations</span>
78
+ </div>
79
+ <span className="px-3 py-1 bg-cyan-950/40 text-cyan-400 border border-cyan-800/30 rounded-full font-bold">
80
+ ENABLED (Fallback)
81
+ </span>
82
+ </div>
83
+
84
+ <div className="flex justify-between items-center bg-slate-950/40 p-4 border border-slate-900 rounded-xl text-xs">
85
+ <div>
86
+ <span className="font-semibold text-slate-200 block">Google API Authorization Scope</span>
87
+ <span className="text-slate-500 block mt-0.5">OAuth scopes to read and send messages</span>
88
+ </div>
89
+ <span className="text-slate-400 bg-slate-900 px-3 py-1 border border-slate-800 rounded-full font-mono">
90
+ gmail.readonly, gmail.compose
91
+ </span>
92
+ </div>
93
+
94
+ <div className="flex justify-between items-center bg-slate-950/40 p-4 border border-slate-900 rounded-xl text-xs">
95
+ <div>
96
+ <span className="font-semibold text-slate-200 block">OpenAI GPT-4o-Mini Engine</span>
97
+ <span className="text-slate-500 block mt-0.5">Drives triage, TL;DR and streaming reply composer</span>
98
+ </div>
99
+ <span className="px-3 py-1 bg-emerald-950/40 text-emerald-400 border border-emerald-800/30 rounded-full font-bold">
100
+ ACTIVE
101
+ </span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ {/* Section 3: Connection Instructions */}
107
+ <div className="glass-panel border-slate-900 rounded-2xl p-6 flex flex-col gap-4">
108
+ <div className="flex items-center gap-2 text-slate-400 font-semibold text-xs uppercase tracking-wider">
109
+ <Shield size={14} />
110
+ <span>Production Setup Checklist</span>
111
+ </div>
112
+ <div className="flex flex-col gap-3 text-xs leading-relaxed text-slate-300">
113
+ <div className="flex gap-2.5 items-start">
114
+ <CheckCircle2 size={15} className="text-purple-400 mt-0.5 shrink-0" />
115
+ <p>Register a Google Developer project, enable Gmail API and OAuth client credentials.</p>
116
+ </div>
117
+ <div className="flex gap-2.5 items-start">
118
+ <CheckCircle2 size={15} className="text-purple-400 mt-0.5 shrink-0" />
119
+ <p>Set <code>MOCK_EMAIL_PROVIDER=false</code> inside the <code>.env</code> file.</p>
120
+ </div>
121
+ <div className="flex gap-2.5 items-start">
122
+ <CheckCircle2 size={15} className="text-purple-400 mt-0.5 shrink-0" />
123
+ <p>Ensure a valid <code>OPENAI_API_KEY</code> is specified to enable AI models.</p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <button
129
+ onClick={handleLogout}
130
+ className="w-full bg-red-950/20 hover:bg-red-950/30 border border-red-950 text-red-400 hover:text-red-300 font-medium py-3 rounded-xl text-xs transition cursor-pointer"
131
+ >
132
+ Log Out Account
133
+ </button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
frontend/app/(auth)/login/page.tsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, Suspense } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { Mail, Lock, Shield, Sparkles, UserPlus, LogIn, Globe } from 'lucide-react';
6
+ import { API_BASE_URL } from '@/app/config';
7
+
8
+ function LoginContent() {
9
+ const router = useRouter();
10
+ const searchParams = useSearchParams();
11
+
12
+ const [isRegisterMode, setIsRegisterMode] = useState(false);
13
+ const [email, setEmail] = useState('');
14
+ const [password, setPassword] = useState('');
15
+ const [name, setName] = useState('');
16
+ const [loading, setLoading] = useState(false);
17
+ const [message, setMessage] = useState({ text: '', type: 'idle' });
18
+
19
+ // Handle redirect from OAuth callback with query parameters
20
+ useEffect(() => {
21
+ const token = searchParams.get('token');
22
+ const userEmail = searchParams.get('email');
23
+ const userId = searchParams.get('userId');
24
+ const error = searchParams.get('error');
25
+
26
+ if (token && userEmail && userId) {
27
+ localStorage.setItem('token', token);
28
+ localStorage.setItem('email', userEmail);
29
+ localStorage.setItem('userId', userId);
30
+
31
+ setMessage({ text: 'OAuth login successful! Redirecting...', type: 'success' });
32
+ setTimeout(() => {
33
+ router.push('/inbox');
34
+ }, 1000);
35
+ } else if (error) {
36
+ setMessage({ text: 'Google OAuth authentication failed. Please try again.', type: 'error' });
37
+ }
38
+ }, [searchParams, router]);
39
+
40
+ const handleOAuthLogin = async () => {
41
+ setLoading(true);
42
+ try {
43
+ const response = await fetch(`${API_BASE_URL}/api/auth/google-login`);
44
+ const data = await response.json();
45
+ if (data.url) {
46
+ window.location.href = data.url;
47
+ } else {
48
+ throw new Error('Authentication endpoint failed to return redirect URL');
49
+ }
50
+ } catch (err: any) {
51
+ console.error(err);
52
+ setMessage({ text: err.message || 'OAuth initialization failed', type: 'error' });
53
+ setLoading(false);
54
+ }
55
+ };
56
+
57
+ const handleCredentialsSubmit = async (e: React.FormEvent) => {
58
+ e.preventDefault();
59
+ if (!email || !password) return;
60
+
61
+ setLoading(true);
62
+ setMessage({ text: '', type: 'idle' });
63
+
64
+ const endpoint = isRegisterMode
65
+ ? `${API_BASE_URL}/api/auth/register`
66
+ : `${API_BASE_URL}/api/auth/login`;
67
+
68
+ const payload = isRegisterMode
69
+ ? JSON.stringify({ email, password, name })
70
+ : new URLSearchParams({ username: email, password: password });
71
+
72
+ try {
73
+ const response = await fetch(endpoint, {
74
+ method: 'POST',
75
+ headers: isRegisterMode
76
+ ? { 'Content-Type': 'application/json' }
77
+ : { 'Content-Type': 'application/x-www-form-urlencoded' },
78
+ body: payload,
79
+ });
80
+
81
+ const data = await response.json();
82
+
83
+ if (!response.ok) {
84
+ throw new Error(data.detail || 'Authentication failed');
85
+ }
86
+
87
+ // Store in localStorage
88
+ localStorage.setItem('token', data.access_token);
89
+ localStorage.setItem('email', data.email);
90
+ localStorage.setItem('userId', data.user_id);
91
+
92
+ setMessage({ text: isRegisterMode ? 'Account registered successfully!' : 'Login successful!', type: 'success' });
93
+ setTimeout(() => {
94
+ router.push('/inbox');
95
+ }, 800);
96
+ } catch (err: any) {
97
+ console.error(err);
98
+ setMessage({ text: err.message || 'Server connection error', type: 'error' });
99
+ } finally {
100
+ setLoading(false);
101
+ }
102
+ };
103
+
104
+ return (
105
+ <div className="min-h-screen bg-[#06060c] flex items-center justify-center relative px-4 overflow-hidden">
106
+ {/* Decorative background glows */}
107
+ <div className="absolute top-1/4 left-1/4 w-96 h-96 bg-purple-900/15 rounded-full blur-[120px] pointer-events-none" />
108
+ <div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-cyan-900/10 rounded-full blur-[120px] pointer-events-none" />
109
+
110
+ {/* Main Glass Card Container */}
111
+ <div className="w-full max-w-md glass-panel rounded-2xl border border-purple-500/10 p-8 flex flex-col gap-6 animate-fade-in-up relative z-10">
112
+ {/* Brand Header */}
113
+ <div className="text-center">
114
+ <div className="inline-flex h-11 w-11 items-center justify-center bg-purple-950/60 border border-purple-500/20 text-purple-400 rounded-xl mb-3 shadow-[0_0_15px_rgba(147,51,234,0.15)]">
115
+ <Shield size={20} className="fill-purple-400/10" />
116
+ </div>
117
+ <h1 className="text-2xl font-bold tracking-tight text-white flex items-center justify-center gap-1.5 leading-snug">
118
+ <span>Antigravity Mail</span>
119
+ <Sparkles size={18} className="text-purple-400" />
120
+ </h1>
121
+ <p className="text-xs text-slate-500 mt-1 font-medium">Agentic AI Email & Triage Assistant</p>
122
+ </div>
123
+
124
+ {/* Status Message */}
125
+ {message.type !== 'idle' && (
126
+ <div className={`p-3.5 rounded-xl border text-xs font-medium text-center ${
127
+ message.type === 'success'
128
+ ? 'bg-emerald-950/20 border-emerald-500/25 text-emerald-400'
129
+ : 'bg-red-950/20 border-red-500/25 text-red-400'
130
+ }`}>
131
+ {message.text}
132
+ </div>
133
+ )}
134
+
135
+ {/* Credentials Form */}
136
+ <form onSubmit={handleCredentialsSubmit} className="flex flex-col gap-4">
137
+ {isRegisterMode && (
138
+ <div className="flex flex-col gap-1.5">
139
+ <label className="text-[10px] uppercase font-bold tracking-wider text-slate-400">Full Name</label>
140
+ <div className="relative">
141
+ <input
142
+ type="text"
143
+ required
144
+ placeholder="John Doe"
145
+ value={name}
146
+ onChange={(e) => setName(e.target.value)}
147
+ className="w-full px-4 py-2.5 rounded-xl text-sm glass-input"
148
+ />
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ <div className="flex flex-col gap-1.5">
154
+ <label className="text-[10px] uppercase font-bold tracking-wider text-slate-400">Email Address</label>
155
+ <div className="relative">
156
+ <Mail className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
157
+ <input
158
+ type="email"
159
+ required
160
+ placeholder="developer@example.com"
161
+ value={email}
162
+ onChange={(e) => setEmail(e.target.value)}
163
+ className="w-full pl-10 pr-4 py-2.5 rounded-xl text-sm glass-input"
164
+ />
165
+ </div>
166
+ </div>
167
+
168
+ <div className="flex flex-col gap-1.5">
169
+ <label className="text-[10px] uppercase font-bold tracking-wider text-slate-400">Password</label>
170
+ <div className="relative">
171
+ <Lock className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
172
+ <input
173
+ type="password"
174
+ required
175
+ placeholder="••••••••"
176
+ value={password}
177
+ onChange={(e) => setPassword(e.target.value)}
178
+ className="w-full pl-10 pr-4 py-2.5 rounded-xl text-sm glass-input"
179
+ />
180
+ </div>
181
+ </div>
182
+
183
+ <button
184
+ type="submit"
185
+ disabled={loading}
186
+ className="glow-btn text-white w-full py-2.5 rounded-xl font-medium text-sm flex items-center justify-center gap-2 mt-2 cursor-pointer disabled:opacity-50"
187
+ >
188
+ {isRegisterMode ? <UserPlus size={16} /> : <LogIn size={16} />}
189
+ <span>{loading ? 'Processing...' : isRegisterMode ? 'Register Account' : 'Sign In'}</span>
190
+ </button>
191
+ </form>
192
+
193
+ {/* Separator */}
194
+ <div className="flex items-center gap-3 text-[10px] text-slate-600 uppercase font-bold tracking-widest my-1">
195
+ <div className="flex-1 h-px bg-slate-900" />
196
+ <span>or</span>
197
+ <div className="flex-1 h-px bg-slate-900" />
198
+ </div>
199
+
200
+ {/* Google OAuth Access */}
201
+ <button
202
+ onClick={handleOAuthLogin}
203
+ disabled={loading}
204
+ className="w-full bg-slate-950/50 hover:bg-slate-900 text-slate-200 border border-slate-800/80 hover:border-slate-700 py-2.5 rounded-xl text-sm font-medium flex items-center justify-center gap-2.5 transition cursor-pointer disabled:opacity-50"
205
+ >
206
+ <Globe size={16} className="text-purple-400" />
207
+ <span>Continue with Google Gmail</span>
208
+ </button>
209
+
210
+ {/* Registration Toggle */}
211
+ <div className="text-center text-xs text-slate-500 mt-2">
212
+ {isRegisterMode ? 'Already have an account?' : "Don't have an account?"}{' '}
213
+ <button
214
+ onClick={() => {
215
+ setIsRegisterMode(!isRegisterMode);
216
+ setMessage({ text: '', type: 'idle' });
217
+ }}
218
+ className="text-purple-400 hover:text-purple-300 font-semibold cursor-pointer outline-none bg-transparent border-none ml-0.5"
219
+ >
220
+ {isRegisterMode ? 'Sign In' : 'Create an Account'}
221
+ </button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ );
226
+ }
227
+
228
+ export default function LoginPage() {
229
+ return (
230
+ <Suspense fallback={
231
+ <div className="min-h-screen bg-[#06060c] flex items-center justify-center text-slate-500">
232
+ <div className="flex flex-col items-center gap-2">
233
+ <div className="h-6 w-6 animate-spin rounded-full border border-purple-500 border-t-transparent" />
234
+ <p className="text-xs">Loading page dependencies...</p>
235
+ </div>
236
+ </div>
237
+ }>
238
+ <LoginContent />
239
+ </Suspense>
240
+ );
241
+ }
frontend/app/config.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
2
+ export const WS_BASE_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000';
frontend/app/favicon.ico ADDED
frontend/app/globals.css ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @theme inline {
4
+ --color-brand-primary: hsl(263, 90%, 55%);
5
+ --color-brand-secondary: hsl(190, 95%, 45%);
6
+ --color-brand-glow: hsl(263, 90%, 75%);
7
+ --color-glass-bg: rgba(13, 13, 24, 0.45);
8
+ --color-glass-border: rgba(255, 255, 255, 0.08);
9
+ --color-glass-border-glow: rgba(147, 51, 234, 0.35);
10
+ --color-inbox-bg: #09090e;
11
+ --color-card-dark: #12121e;
12
+ }
13
+
14
+ :root {
15
+ --background: #06060c;
16
+ --foreground: #f3f4f6;
17
+ }
18
+
19
+ body {
20
+ background-color: var(--background);
21
+ color: var(--foreground);
22
+ font-family: 'Inter', system-ui, sans-serif;
23
+ overflow-x: hidden;
24
+ }
25
+
26
+ /* Glassmorphism utility classes */
27
+ .glass-panel {
28
+ background: rgba(15, 15, 27, 0.65);
29
+ backdrop-filter: blur(16px);
30
+ -webkit-backdrop-filter: blur(16px);
31
+ border: 1px solid rgba(255, 255, 255, 0.06);
32
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
33
+ }
34
+
35
+ .glass-panel-hover:hover {
36
+ border-color: rgba(147, 51, 234, 0.25);
37
+ box-shadow: 0 8px 32px 0 rgba(147, 51, 234, 0.1);
38
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
39
+ }
40
+
41
+ .glass-input {
42
+ background: rgba(255, 255, 255, 0.03);
43
+ border: 1px solid rgba(255, 255, 255, 0.08);
44
+ color: #fff;
45
+ transition: all 0.2s ease;
46
+ }
47
+
48
+ .glass-input:focus {
49
+ background: rgba(255, 255, 255, 0.05);
50
+ border-color: #9333ea;
51
+ box-shadow: 0 0 10px rgba(147, 51, 234, 0.2);
52
+ outline: none;
53
+ }
54
+
55
+ /* Glowing text and states */
56
+ .glow-text {
57
+ text-shadow: 0 0 15px rgba(147, 51, 234, 0.6);
58
+ }
59
+
60
+ .glow-btn {
61
+ background: linear-gradient(135deg, #7c3aed 0%, #06b6d4 100%);
62
+ box-shadow: 0 4px 20px rgba(124, 58, 237, 0.3);
63
+ transition: all 0.3s ease;
64
+ }
65
+
66
+ .glow-btn:hover {
67
+ transform: translateY(-1px);
68
+ box-shadow: 0 6px 24px rgba(124, 58, 237, 0.5), 0 0 10px rgba(6, 182, 212, 0.3);
69
+ }
70
+
71
+ /* Custom Scrollbars */
72
+ ::-webkit-scrollbar {
73
+ width: 6px;
74
+ height: 6px;
75
+ }
76
+
77
+ ::-webkit-scrollbar-track {
78
+ background: rgba(15, 15, 27, 0.4);
79
+ }
80
+
81
+ ::-webkit-scrollbar-thumb {
82
+ background: rgba(255, 255, 255, 0.1);
83
+ border-radius: 4px;
84
+ }
85
+
86
+ ::-webkit-scrollbar-thumb:hover {
87
+ background: rgba(147, 51, 234, 0.4);
88
+ }
89
+
90
+ /* Pulse animation for active agents */
91
+ @keyframes agent-glow {
92
+ 0%, 100% {
93
+ box-shadow: 0 0 8px rgba(147, 51, 234, 0.4);
94
+ border-color: rgba(147, 51, 234, 0.4);
95
+ }
96
+ 50% {
97
+ box-shadow: 0 0 20px rgba(147, 51, 234, 0.8), 0 0 10px rgba(6, 182, 212, 0.4);
98
+ border-color: rgba(6, 182, 212, 0.6);
99
+ }
100
+ }
101
+
102
+ .animate-agent-glow {
103
+ animation: agent-glow 3s infinite ease-in-out;
104
+ }
105
+
106
+ /* Custom animations */
107
+ @keyframes fade-in-up {
108
+ from {
109
+ opacity: 0;
110
+ transform: translateY(12px);
111
+ }
112
+ to {
113
+ opacity: 1;
114
+ transform: translateY(0);
115
+ }
116
+ }
117
+
118
+ .animate-fade-in-up {
119
+ animation: fade-in-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
120
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Antigravity Mail - AI Email Agent",
17
+ description: "Agentic AI Email and Inbox Triage Assistant",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html
27
+ lang="en"
28
+ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
29
+ >
30
+ <body className="min-h-full flex flex-col">{children}</body>
31
+ </html>
32
+ );
33
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ export default function Home() {
7
+ const router = useRouter();
8
+
9
+ useEffect(() => {
10
+ const token = localStorage.getItem('token');
11
+ if (token) {
12
+ router.push('/inbox');
13
+ } else {
14
+ router.push('/login');
15
+ }
16
+ }, [router]);
17
+
18
+ return (
19
+ <div className="min-h-screen bg-[#06060c] flex items-center justify-center text-slate-500">
20
+ <div className="flex flex-col items-center gap-2">
21
+ <div className="h-6 w-6 animate-spin rounded-full border border-purple-500 border-t-transparent" />
22
+ <p className="text-xs">Redirecting to workspace...</p>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
frontend/components/AgentRunTimeline.tsx ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Mail, Search, Sparkles, Database, ShieldCheck, Terminal, AlertCircle } from 'lucide-react';
3
+ import { API_BASE_URL } from '@/app/config';
4
+
5
+ interface AgentLogItem {
6
+ step: number;
7
+ thought: string;
8
+ action: string;
9
+ result: string;
10
+ timestamp: string;
11
+ }
12
+
13
+ interface AgentRunTimelineProps {
14
+ runId: string;
15
+ }
16
+
17
+ export default function AgentRunTimeline({ runId }: AgentRunTimelineProps) {
18
+ const [logs, setLogs] = useState<AgentLogItem[]>([]);
19
+ const [isLoading, setIsLoading] = useState(false);
20
+ const [error, setError] = useState(false);
21
+
22
+ useEffect(() => {
23
+ const fetchLogs = async () => {
24
+ setIsLoading(true);
25
+ setError(false);
26
+
27
+ const token = localStorage.getItem('token') || '';
28
+ try {
29
+ const response = await fetch(`${API_BASE_URL}/api/agent/runs/${runId}/logs`, {
30
+ headers: {
31
+ 'Authorization': `Bearer ${token}`,
32
+ },
33
+ });
34
+
35
+ if (!response.ok) {
36
+ throw new Error('Failed to fetch run logs');
37
+ }
38
+
39
+ const data = await response.json();
40
+ setLogs(data);
41
+ } catch (err) {
42
+ console.error(err);
43
+ setError(true);
44
+ } finally {
45
+ setIsLoading(false);
46
+ }
47
+ };
48
+
49
+ if (runId) {
50
+ fetchLogs();
51
+ // Poll every 3 seconds while active
52
+ const interval = setInterval(fetchLogs, 3000);
53
+ return () => clearInterval(interval);
54
+ }
55
+ }, [runId]);
56
+
57
+ const getActionIcon = (action: string) => {
58
+ switch (action.toUpperCase()) {
59
+ case 'READ_EMAIL':
60
+ return <Mail size={14} className="text-cyan-400" />;
61
+ case 'RETRIEVE_CONTEXT':
62
+ return <Database size={14} className="text-indigo-400" />;
63
+ case 'LLM_TRIAGE':
64
+ return <Sparkles size={14} className="text-purple-400" />;
65
+ case 'COMMIT_CHANGES':
66
+ return <ShieldCheck size={14} className="text-emerald-400" />;
67
+ default:
68
+ return <Terminal size={14} className="text-slate-400" />;
69
+ }
70
+ };
71
+
72
+ const getActionColor = (action: string) => {
73
+ switch (action.toUpperCase()) {
74
+ case 'READ_EMAIL':
75
+ return 'border-cyan-500/30 bg-cyan-950/20';
76
+ case 'RETRIEVE_CONTEXT':
77
+ return 'border-indigo-500/30 bg-indigo-950/20';
78
+ case 'LLM_TRIAGE':
79
+ return 'border-purple-500/30 bg-purple-950/20';
80
+ case 'COMMIT_CHANGES':
81
+ return 'border-emerald-500/30 bg-emerald-950/20';
82
+ default:
83
+ return 'border-slate-800 bg-slate-950/40';
84
+ }
85
+ };
86
+
87
+ if (isLoading && logs.length === 0) {
88
+ return (
89
+ <div className="flex items-center justify-center p-8 text-slate-500">
90
+ <div className="flex items-center gap-2">
91
+ <div className="h-4 w-4 animate-spin rounded-full border border-purple-500 border-t-transparent" />
92
+ <span className="text-xs">Loading audit trail...</span>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ if (error && logs.length === 0) {
99
+ return (
100
+ <div className="flex items-center justify-center p-6 text-red-400 gap-2 border border-red-950/30 bg-red-950/10 rounded-xl">
101
+ <AlertCircle size={16} />
102
+ <span className="text-xs">Could not fetch logs for this agent execution.</span>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <div className="flex flex-col gap-4">
109
+ <h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
110
+ <Terminal size={14} />
111
+ <span>Agent Thinking Audit Trail</span>
112
+ </h4>
113
+
114
+ {logs.length === 0 ? (
115
+ <p className="text-xs text-slate-500 italic">No agent log steps found for this execution run.</p>
116
+ ) : (
117
+ <div className="relative border-l border-slate-800 ml-3.5 pl-6 flex flex-col gap-6.5">
118
+ {logs.map((log) => (
119
+ <div key={log.step} className="relative group">
120
+ {/* Vertical line connector bullet */}
121
+ <div className={`absolute -left-[35px] top-0 h-7 w-7 rounded-full border flex items-center justify-center transition-transform group-hover:scale-110 ${getActionColor(log.action)}`}>
122
+ {getActionIcon(log.action)}
123
+ </div>
124
+
125
+ {/* Node Contents */}
126
+ <div>
127
+ <div className="flex items-center gap-2.5 mb-1">
128
+ <span className="text-xs font-semibold text-slate-200">
129
+ Step {log.step}: {log.thought}
130
+ </span>
131
+ <span className="text-[10px] text-slate-500 font-mono">
132
+ {new Date(log.timestamp).toLocaleTimeString()}
133
+ </span>
134
+ </div>
135
+ <div className="bg-slate-950/35 border border-slate-900/60 p-3 rounded-xl text-[11px] text-slate-400 font-mono leading-relaxed max-w-full overflow-x-auto">
136
+ <span className="text-slate-500 block mb-1">Action: {log.action}</span>
137
+ <span className="text-slate-300 block">{log.result}</span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ ))}
142
+ </div>
143
+ )}
144
+ </div>
145
+ );
146
+ }
frontend/components/AgentStatusBar.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Cpu, RefreshCw, Terminal, Zap } from 'lucide-react';
3
+
4
+ interface AgentStatus {
5
+ status: string;
6
+ progress: number;
7
+ active: boolean;
8
+ runId?: string;
9
+ }
10
+
11
+ interface AgentStatusBarProps {
12
+ statusData: AgentStatus;
13
+ isSyncing: boolean;
14
+ onTriggerSync: () => Promise<void>;
15
+ onViewLogs: (runId: string) => void;
16
+ }
17
+
18
+ export default function AgentStatusBar({
19
+ statusData,
20
+ isSyncing,
21
+ onTriggerSync,
22
+ onViewLogs,
23
+ }: AgentStatusBarProps) {
24
+ return (
25
+ <div className="glass-panel rounded-2xl p-6 flex flex-col gap-5 border border-purple-500/10">
26
+ {/* Header / Active Indicator */}
27
+ <div className="flex items-center justify-between">
28
+ <div className="flex items-center gap-3">
29
+ <div className="relative flex h-3 w-3">
30
+ {statusData.active ? (
31
+ <>
32
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span>
33
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-purple-500"></span>
34
+ </>
35
+ ) : (
36
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-slate-600"></span>
37
+ )}
38
+ </div>
39
+ <div>
40
+ <h3 className="font-semibold text-sm tracking-wide text-slate-200">AGENT STATUS</h3>
41
+ <p className="text-xs text-slate-400">GPT-4o-Mini Engine</p>
42
+ </div>
43
+ </div>
44
+ <div className="flex items-center gap-1 bg-purple-950/40 px-2.5 py-1 rounded-full border border-purple-800/30 text-purple-300 text-xs">
45
+ <Zap size={12} className="fill-purple-300/20" />
46
+ <span>Active</span>
47
+ </div>
48
+ </div>
49
+
50
+ {/* Status Bar Indicator */}
51
+ <div className={`p-4 rounded-xl border ${statusData.active ? 'bg-purple-950/20 border-purple-500/25 animate-agent-glow' : 'bg-slate-950/40 border-slate-800/40'}`}>
52
+ <p className="text-sm font-medium text-slate-100 truncate mb-2">
53
+ {statusData.status || "Agent is sleeping. Waiting for mail..."}
54
+ </p>
55
+
56
+ {/* Progress Bar */}
57
+ <div className="w-full bg-slate-900 rounded-full h-2 overflow-hidden">
58
+ <div
59
+ className="h-full bg-gradient-to-r from-purple-500 to-cyan-500 transition-all duration-500 ease-out"
60
+ style={{ width: `${statusData.progress}%` }}
61
+ />
62
+ </div>
63
+
64
+ <div className="flex justify-between items-center mt-2 text-xs text-slate-400">
65
+ <span>Progress</span>
66
+ <span className="font-mono">{statusData.progress}%</span>
67
+ </div>
68
+ </div>
69
+
70
+ {/* System Metrics */}
71
+ <div className="grid grid-cols-2 gap-3 text-xs">
72
+ <div className="bg-slate-950/30 border border-slate-900 p-3 rounded-lg">
73
+ <span className="text-slate-500 block mb-1">Worker Threads</span>
74
+ <span className="font-mono text-slate-300 font-medium">Celery: Active</span>
75
+ </div>
76
+ <div className="bg-slate-950/30 border border-slate-900 p-3 rounded-lg">
77
+ <span className="text-slate-500 block mb-1">RAG Context</span>
78
+ <span className="font-mono text-slate-300 font-medium">MongoDB: Live</span>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Quick Action Trigger */}
83
+ <div className="flex flex-col gap-2.5 mt-2">
84
+ <button
85
+ onClick={onTriggerSync}
86
+ disabled={isSyncing || statusData.active}
87
+ className="glow-btn text-white w-full font-medium py-2.5 px-4 rounded-xl flex items-center justify-center gap-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
88
+ >
89
+ <RefreshCw size={15} className={`transition-transform ${isSyncing ? 'animate-spin' : ''}`} />
90
+ <span>{isSyncing ? 'Syncing Mailbox...' : 'Sync Mailbox Now'}</span>
91
+ </button>
92
+
93
+ {statusData.runId && (
94
+ <button
95
+ onClick={() => onViewLogs(statusData.runId!)}
96
+ className="w-full bg-slate-950/50 hover:bg-slate-900 border border-slate-800 text-slate-300 hover:text-slate-100 transition py-2 px-4 rounded-xl flex items-center justify-center gap-2 text-sm cursor-pointer"
97
+ >
98
+ <Terminal size={14} />
99
+ <span>Audit Agent Logs</span>
100
+ </button>
101
+ )}
102
+ </div>
103
+ </div>
104
+ );
105
+ }
frontend/components/EmailDetail.tsx ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Eye, FileText, CheckSquare, Sparkles, User, Calendar, Paperclip } from 'lucide-react';
3
+
4
+ interface EmailDetailData {
5
+ id: string;
6
+ subject?: string;
7
+ sender: string;
8
+ received_at: string;
9
+ category: string;
10
+ is_read: boolean;
11
+ is_starred: boolean;
12
+ thread_id?: string;
13
+ has_attachment: boolean;
14
+ body_html: string;
15
+ body_text: string;
16
+ summary?: string;
17
+ action_items: string[];
18
+ }
19
+
20
+ interface EmailDetailProps {
21
+ email: EmailDetailData;
22
+ isLoading: boolean;
23
+ }
24
+
25
+ export default function EmailDetail({ email, isLoading }: EmailDetailProps) {
26
+ const [activeTab, setActiveTab] = useState<'html' | 'text'>('html');
27
+ const [checkedItems, setCheckedItems] = useState<Record<number, boolean>>({});
28
+
29
+ // Reset checked items when selected email changes
30
+ useEffect(() => {
31
+ setCheckedItems({});
32
+ if (email.body_html) {
33
+ setActiveTab('html');
34
+ } else {
35
+ setActiveTab('text');
36
+ }
37
+ }, [email.id, email.body_html]);
38
+
39
+ const toggleCheck = (idx: number) => {
40
+ setCheckedItems((prev) => ({
41
+ ...prev,
42
+ [idx]: !prev[idx],
43
+ }));
44
+ };
45
+
46
+ const getCategoryStyle = (cat: string) => {
47
+ switch (cat.toLowerCase()) {
48
+ case 'urgent':
49
+ return 'bg-red-950/40 border border-red-500/30 text-red-400';
50
+ case 'invoice':
51
+ return 'bg-cyan-950/40 border border-cyan-500/30 text-cyan-400';
52
+ case 'newsletter':
53
+ return 'bg-slate-900 border border-slate-700/60 text-slate-400';
54
+ default:
55
+ return 'bg-emerald-950/40 border border-emerald-500/30 text-emerald-400';
56
+ }
57
+ };
58
+
59
+ if (isLoading) {
60
+ return (
61
+ <div className="flex items-center justify-center h-full text-slate-500">
62
+ <div className="flex flex-col items-center gap-3">
63
+ <div className="h-8 w-8 animate-spin rounded-full border-2 border-purple-500 border-t-transparent" />
64
+ <p className="text-xs">Loading email content...</p>
65
+ </div>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ return (
71
+ <div className="flex flex-col h-full bg-slate-950/30 border border-slate-900 rounded-2xl overflow-hidden animate-fade-in-up">
72
+ {/* Subject Header */}
73
+ <div className="p-6 border-b border-slate-900 bg-slate-950/20">
74
+ <div className="flex items-center gap-3 mb-2.5">
75
+ <span className={`px-2.5 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider ${getCategoryStyle(email.category)}`}>
76
+ {email.category}
77
+ </span>
78
+ <span className="text-xs text-slate-500 flex items-center gap-1">
79
+ <Calendar size={12} />
80
+ {new Date(email.received_at).toLocaleString()}
81
+ </span>
82
+ </div>
83
+ <h2 className="text-xl font-bold text-white tracking-tight leading-snug">
84
+ {email.subject || '(No Subject)'}
85
+ </h2>
86
+ </div>
87
+
88
+ {/* Sender Header */}
89
+ <div className="px-6 py-4 border-b border-slate-900/60 flex items-center gap-3 bg-slate-950/10">
90
+ <div className="h-9 w-9 rounded-full bg-purple-950/50 border border-purple-800/40 flex items-center justify-center text-purple-400 font-bold text-sm">
91
+ <User size={16} />
92
+ </div>
93
+ <div className="min-w-0">
94
+ <span className="text-sm font-semibold text-slate-200 block truncate">
95
+ {email.sender}
96
+ </span>
97
+ <span className="text-[11px] text-slate-500 block">
98
+ to me
99
+ </span>
100
+ </div>
101
+ </div>
102
+
103
+ {/* Main Grid: Body on Left, AI Panel on Right */}
104
+ <div className="flex-1 flex overflow-hidden">
105
+ {/* Left Side: Email Body Content */}
106
+ <div className="flex-1 flex flex-col border-r border-slate-900 overflow-hidden">
107
+ {/* Content Format Toggle Tabs */}
108
+ <div className="flex px-4 py-2 gap-2 border-b border-slate-900/60 bg-slate-950/15 text-xs justify-end">
109
+ <button
110
+ onClick={() => setActiveTab('html')}
111
+ disabled={!email.body_html}
112
+ className={`px-2.5 py-1 rounded flex items-center gap-1 cursor-pointer transition ${
113
+ activeTab === 'html' ? 'bg-slate-800 text-white' : 'text-slate-500 hover:text-slate-300'
114
+ }`}
115
+ >
116
+ <Eye size={12} />
117
+ <span>HTML View</span>
118
+ </button>
119
+ <button
120
+ onClick={() => setActiveTab('text')}
121
+ className={`px-2.5 py-1 rounded flex items-center gap-1 cursor-pointer transition ${
122
+ activeTab === 'text' ? 'bg-slate-800 text-white' : 'text-slate-500 hover:text-slate-300'
123
+ }`}
124
+ >
125
+ <FileText size={12} />
126
+ <span>Plain Text</span>
127
+ </button>
128
+ </div>
129
+
130
+ {/* Body Viewer */}
131
+ <div className="flex-1 p-6 overflow-y-auto bg-slate-950/10">
132
+ {activeTab === 'html' && email.body_html ? (
133
+ <div
134
+ className="prose prose-invert max-w-none text-slate-300 text-sm leading-relaxed"
135
+ dangerouslySetInnerHTML={{ __html: email.body_html }}
136
+ />
137
+ ) : (
138
+ <pre className="whitespace-pre-wrap font-sans text-sm text-slate-300 leading-relaxed">
139
+ {email.body_text || "No email body text content found."}
140
+ </pre>
141
+ )}
142
+
143
+ {/* Attachments Section */}
144
+ {email.has_attachment && (
145
+ <div className="mt-8 pt-6 border-t border-slate-900">
146
+ <h5 className="text-xs font-semibold text-slate-400 mb-3 flex items-center gap-1.5">
147
+ <Paperclip size={12} />
148
+ <span>Attachments (1)</span>
149
+ </h5>
150
+ <div className="inline-flex items-center gap-3 bg-slate-950/40 hover:bg-slate-900 border border-slate-800 p-2.5 rounded-xl transition cursor-pointer">
151
+ <div className="p-2 bg-purple-950/50 text-purple-400 rounded-lg">
152
+ <FileText size={16} />
153
+ </div>
154
+ <div>
155
+ <span className="text-xs font-medium text-slate-300 block">document.pdf</span>
156
+ <span className="text-[10px] text-slate-500 block">100 KB • PDF File</span>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ )}
161
+ </div>
162
+ </div>
163
+
164
+ {/* Right Side: AI Panel (TL;DR & Action Items) */}
165
+ <div className="w-80 flex flex-col bg-slate-950/20 overflow-y-auto p-5 gap-5">
166
+ {/* Summary Box */}
167
+ <div className="glass-panel border-purple-500/10 rounded-xl p-4 flex flex-col gap-2.5">
168
+ <div className="flex items-center gap-2 text-purple-400 font-semibold text-xs uppercase tracking-wider">
169
+ <Sparkles size={14} className="fill-purple-400/20" />
170
+ <span>AI TL;DR Summary</span>
171
+ </div>
172
+ <p className="text-xs text-slate-300 leading-relaxed">
173
+ {email.summary || "Agent hasn't generated a summary yet. Wait for triage logs to update."}
174
+ </p>
175
+ </div>
176
+
177
+ {/* Action Items Checklist */}
178
+ <div className="glass-panel border-purple-500/10 rounded-xl p-4 flex flex-col gap-3">
179
+ <div className="flex items-center gap-2 text-cyan-400 font-semibold text-xs uppercase tracking-wider">
180
+ <CheckSquare size={14} />
181
+ <span>Task Checklist ({email.action_items.length})</span>
182
+ </div>
183
+ {email.action_items.length === 0 ? (
184
+ <p className="text-xs text-slate-500 italic">No tasks extracted.</p>
185
+ ) : (
186
+ <div className="flex flex-col gap-2.5">
187
+ {email.action_items.map((item, idx) => {
188
+ const isChecked = checkedItems[idx] || false;
189
+ return (
190
+ <div
191
+ key={idx}
192
+ onClick={() => toggleCheck(idx)}
193
+ className="flex items-start gap-2.5 cursor-pointer group"
194
+ >
195
+ <div className={`mt-0.5 h-4 w-4 rounded border flex items-center justify-center transition shrink-0 ${
196
+ isChecked
197
+ ? 'bg-cyan-500 border-cyan-500 text-slate-950'
198
+ : 'border-slate-700 bg-transparent group-hover:border-slate-500'
199
+ }`}>
200
+ {isChecked && (
201
+ <svg className="w-3 h-3 fill-current stroke-[3px]" viewBox="0 0 20 20">
202
+ <path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />
203
+ </svg>
204
+ )}
205
+ </div>
206
+ <span className={`text-xs select-none transition ${isChecked ? 'line-through text-slate-500' : 'text-slate-300'}`}>
207
+ {item}
208
+ </span>
209
+ </div>
210
+ );
211
+ })}
212
+ </div>
213
+ )}
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ );
219
+ }
frontend/components/EmailList.tsx ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Mail, Star, Search, Paperclip } from 'lucide-react';
3
+
4
+ interface EmailListItem {
5
+ id: str;
6
+ subject?: str;
7
+ sender: str;
8
+ received_at: str;
9
+ category: str;
10
+ is_read: bool;
11
+ is_starred: bool;
12
+ thread_id?: str;
13
+ has_attachment: bool;
14
+ }
15
+
16
+ interface EmailListProps {
17
+ emails: EmailListItem[];
18
+ selectedId: string | null;
19
+ onSelectEmail: (id: string) => void;
20
+ onToggleStar: (id: string, isStarred: boolean, e: React.MouseEvent) => void;
21
+ activeCategory: string | null;
22
+ setActiveCategory: (category: string | null) => void;
23
+ }
24
+
25
+ export default function EmailList({
26
+ emails,
27
+ selectedId,
28
+ onSelectEmail,
29
+ onToggleStar,
30
+ activeCategory,
31
+ setActiveCategory,
32
+ }: EmailListProps) {
33
+ const [searchQuery, setSearchQuery] = useState('');
34
+
35
+ // Filter categories
36
+ const categories = [
37
+ { label: 'All Mail', value: null },
38
+ { label: 'Urgent', value: 'urgent', color: 'border-red-500/30 text-red-400 bg-red-950/20' },
39
+ { label: 'Personal', value: 'personal', color: 'border-emerald-500/30 text-emerald-400 bg-emerald-950/20' },
40
+ { label: 'Invoices', value: 'invoice', color: 'border-cyan-500/30 text-cyan-400 bg-cyan-950/20' },
41
+ { label: 'Newsletters', value: 'newsletter', color: 'border-slate-500/30 text-slate-400 bg-slate-950/10' },
42
+ ];
43
+
44
+ // Apply local text search filter
45
+ const filteredEmails = emails.filter((email) => {
46
+ const matchesSearch =
47
+ (email.subject || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
48
+ email.sender.toLowerCase().includes(searchQuery.toLowerCase());
49
+ return matchesSearch;
50
+ });
51
+
52
+ const getCategoryStyle = (cat: string) => {
53
+ switch (cat.toLowerCase()) {
54
+ case 'urgent':
55
+ return 'bg-red-950/30 border border-red-500/25 text-red-400';
56
+ case 'invoice':
57
+ return 'bg-cyan-950/30 border border-cyan-500/25 text-cyan-400';
58
+ case 'newsletter':
59
+ return 'bg-slate-900 border border-slate-700/50 text-slate-400';
60
+ default:
61
+ return 'bg-emerald-950/30 border border-emerald-500/25 text-emerald-400';
62
+ }
63
+ };
64
+
65
+ const formatSender = (senderStr: string) => {
66
+ // Return display name or clean email
67
+ const match = senderStr.match(/^([^<]+)/);
68
+ return match ? match[1].trim() : senderStr;
69
+ };
70
+
71
+ const formatTime = (isoString: string) => {
72
+ try {
73
+ const date = new Date(isoString);
74
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
75
+ } catch {
76
+ return '';
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div className="flex flex-col h-full bg-slate-950/30 border border-slate-900 rounded-2xl overflow-hidden">
82
+ {/* Search Header */}
83
+ <div className="p-4 border-b border-slate-900 flex gap-3 items-center bg-slate-950/20">
84
+ <div className="relative flex-1">
85
+ <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
86
+ <input
87
+ type="text"
88
+ placeholder="Search email subject or sender..."
89
+ value={searchQuery}
90
+ onChange={(e) => setSearchQuery(e.target.value)}
91
+ className="w-full pl-10 pr-4 py-2 text-sm rounded-xl glass-input placeholder-slate-500"
92
+ />
93
+ </div>
94
+ </div>
95
+
96
+ {/* Tabs */}
97
+ <div className="flex px-4 py-3 gap-2 overflow-x-auto border-b border-slate-900/60 bg-slate-950/10">
98
+ {categories.map((cat) => {
99
+ const isActive = activeCategory === cat.value;
100
+ return (
101
+ <button
102
+ key={cat.label}
103
+ onClick={() => setActiveCategory(cat.value)}
104
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium border whitespace-nowrap cursor-pointer transition-all ${
105
+ isActive
106
+ ? 'bg-purple-600/20 border-purple-500/60 text-purple-200'
107
+ : 'bg-transparent border-slate-900 text-slate-400 hover:text-slate-200 hover:border-slate-800'
108
+ }`}
109
+ >
110
+ {cat.label}
111
+ </button>
112
+ );
113
+ })}
114
+ </div>
115
+
116
+ {/* Email Scrollable List */}
117
+ <div className="flex-1 overflow-y-auto divide-y divide-slate-900/60">
118
+ {filteredEmails.length === 0 ? (
119
+ <div className="flex flex-col items-center justify-center h-48 text-slate-500 gap-2">
120
+ <Mail size={24} className="stroke-[1.5]" />
121
+ <p className="text-xs">No emails found in this category.</p>
122
+ </div>
123
+ ) : (
124
+ filteredEmails.map((email) => {
125
+ const isSelected = selectedId === email.id;
126
+ return (
127
+ <div
128
+ key={email.id}
129
+ onClick={() => onSelectEmail(email.id)}
130
+ className={`p-4 flex gap-3 transition-all relative cursor-pointer group ${
131
+ isSelected
132
+ ? 'bg-purple-950/10 border-l-2 border-purple-500'
133
+ : 'hover:bg-slate-900/40 border-l-2 border-transparent'
134
+ }`}
135
+ >
136
+ {/* Unread indicator */}
137
+ {!email.is_read && (
138
+ <span className="absolute top-5 left-1 h-2 w-2 rounded-full bg-purple-500 shadow-[0_0_8px_#a855f7]" />
139
+ )}
140
+
141
+ <div className="flex-1 min-w-0">
142
+ <div className="flex items-center justify-between gap-2 mb-1.5">
143
+ <span className={`font-semibold text-xs ${!email.is_read ? 'text-white' : 'text-slate-300'}`}>
144
+ {formatSender(email.sender)}
145
+ </span>
146
+ <span className="text-[10px] text-slate-500 font-medium">
147
+ {formatTime(email.received_at)}
148
+ </span>
149
+ </div>
150
+
151
+ <h4 className={`text-sm mb-1.5 truncate ${!email.is_read ? 'font-semibold text-white' : 'text-slate-300'}`}>
152
+ {email.subject || '(No Subject)'}
153
+ </h4>
154
+
155
+ <div className="flex items-center justify-between mt-2.5">
156
+ {/* Category Label */}
157
+ <span className={`px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider ${getCategoryStyle(email.category)}`}>
158
+ {email.category}
159
+ </span>
160
+
161
+ {/* Star & Attachment indicator */}
162
+ <div className="flex items-center gap-2 text-slate-500 group-hover:text-slate-400">
163
+ {email.has_attachment && <Paperclip size={12} className="stroke-[1.5]" />}
164
+ <button
165
+ onClick={(e) => onToggleStar(email.id, !email.is_starred, e)}
166
+ className="hover:scale-115 transition"
167
+ >
168
+ <Star
169
+ size={14}
170
+ className={email.is_starred ? 'fill-amber-400 stroke-amber-400' : 'stroke-[1.5]'}
171
+ />
172
+ </button>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ );
178
+ })
179
+ )}
180
+ </div>
181
+ </div>
182
+ );
183
+ }
frontend/components/StreamingDraft.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Send, Sparkles, AlertCircle, CheckCircle } from 'lucide-react';
3
+ import { API_BASE_URL } from '@/app/config';
4
+
5
+ interface StreamingDraftProps {
6
+ emailId: string;
7
+ emailSubject?: string;
8
+ emailSender: string;
9
+ onSendEmail: (to: string, subject: string, body: string) => Promise<boolean>;
10
+ }
11
+
12
+ export default function StreamingDraft({
13
+ emailId,
14
+ emailSubject,
15
+ emailSender,
16
+ onSendEmail,
17
+ }: StreamingDraftProps) {
18
+ const [tone, setTone] = useState<'professional' | 'casual' | 'short'>('professional');
19
+ const [draftContent, setDraftContent] = useState('');
20
+ const [isGenerating, setIsGenerating] = useState(false);
21
+ const [isSending, setIsSending] = useState(false);
22
+ const [sendStatus, setSendStatus] = useState<'idle' | 'success' | 'error'>('idle');
23
+ const [errorMsg, setErrorMsg] = useState('');
24
+
25
+ // Clear draft when email selection changes
26
+ useEffect(() => {
27
+ setDraftContent('');
28
+ setSendStatus('idle');
29
+ }, [emailId]);
30
+
31
+ const generateDraft = async () => {
32
+ setIsGenerating(true);
33
+ setDraftContent('');
34
+ setSendStatus('idle');
35
+
36
+ // Connect to FastAPI SSE endpoint
37
+ const url = `${API_BASE_URL}/api/emails/${emailId}/draft?tone=${tone}`;
38
+
39
+ // Access token from localStorage
40
+ const token = localStorage.getItem('token') || '';
41
+
42
+ try {
43
+ const response = await fetch(url, {
44
+ headers: {
45
+ 'Authorization': `Bearer ${token}`,
46
+ },
47
+ });
48
+
49
+ if (!response.ok) {
50
+ throw new Error('Failed to generate draft stream.');
51
+ }
52
+
53
+ const reader = response.body?.getReader();
54
+ const decoder = new TextDecoder();
55
+ if (!reader) {
56
+ throw new Error('No readable stream body.');
57
+ }
58
+
59
+ let done = false;
60
+ let fullText = '';
61
+
62
+ while (!done) {
63
+ const { value, done: doneReading } = await reader.read();
64
+ done = doneReading;
65
+ if (value) {
66
+ const chunkStr = decoder.decode(value);
67
+ // Parse Server Sent Events format "data: text\n\n"
68
+ const lines = chunkStr.split('\n');
69
+ for (const line of lines) {
70
+ if (line.startsWith('data: ')) {
71
+ const content = line.substring(6);
72
+ fullText += content;
73
+ setDraftContent(fullText);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ } catch (err: any) {
79
+ console.error(err);
80
+ setErrorMsg(err.message || 'Connection lost during drafting.');
81
+ setSendStatus('error');
82
+ } finally {
83
+ setIsGenerating(false);
84
+ }
85
+ };
86
+
87
+ const handleSend = async () => {
88
+ if (!draftContent.trim()) return;
89
+
90
+ setIsSending(true);
91
+ setSendStatus('idle');
92
+
93
+ // Extract target email address
94
+ let toEmail = emailSender;
95
+ const match = emailSender.match(/<([^>]+)>/);
96
+ if (match) {
97
+ toEmail = match[1];
98
+ }
99
+
100
+ const subject = emailSubject ? `Re: ${emailSubject}` : 'Re: Email';
101
+ const success = await onSendEmail(toEmail, subject, draftContent);
102
+
103
+ setIsSending(false);
104
+ if (success) {
105
+ setSendStatus('success');
106
+ } else {
107
+ setErrorMsg('Failed to deliver reply.');
108
+ setSendStatus('error');
109
+ }
110
+ };
111
+
112
+ const tones = [
113
+ { label: '💼 Professional', value: 'professional' },
114
+ { label: '☕ Casual', value: 'casual' },
115
+ { label: '⚡ Short / Quick', value: 'short' },
116
+ ];
117
+
118
+ return (
119
+ <div className="glass-panel border-purple-500/10 rounded-2xl p-6 flex flex-col gap-4">
120
+ <div className="flex items-center justify-between">
121
+ <div className="flex items-center gap-2 text-purple-400 font-semibold text-sm">
122
+ <Sparkles size={16} className="fill-purple-400/20" />
123
+ <span>AI Streaming Reply Composer</span>
124
+ </div>
125
+ <div className="text-xs text-slate-500">
126
+ Powered by GPT-4o-Mini
127
+ </div>
128
+ </div>
129
+
130
+ {/* Tone Selection */}
131
+ <div className="flex gap-2.5 bg-slate-950/40 border border-slate-900/60 p-1 rounded-xl">
132
+ {tones.map((t) => (
133
+ <button
134
+ key={t.value}
135
+ disabled={isGenerating || isSending}
136
+ onClick={() => setTone(t.value as any)}
137
+ className={`flex-1 text-center py-2 text-xs font-medium rounded-lg cursor-pointer transition ${
138
+ tone === t.value
139
+ ? 'bg-purple-600/20 text-purple-300 border border-purple-500/20'
140
+ : 'text-slate-500 hover:text-slate-300 border border-transparent'
141
+ }`}
142
+ >
143
+ {t.label}
144
+ </button>
145
+ ))}
146
+ </div>
147
+
148
+ {/* Generator trigger button */}
149
+ {draftContent === '' && !isGenerating && (
150
+ <button
151
+ onClick={generateDraft}
152
+ className="glow-btn text-white py-3 rounded-xl font-medium flex items-center justify-center gap-2 text-sm cursor-pointer"
153
+ >
154
+ <Sparkles size={14} />
155
+ <span>Draft Response in {tone.toUpperCase()} Tone</span>
156
+ </button>
157
+ )}
158
+
159
+ {/* Draft text editor */}
160
+ {(draftContent !== '' || isGenerating) && (
161
+ <div className="flex flex-col gap-3">
162
+ <textarea
163
+ value={draftContent}
164
+ onChange={(e) => setDraftContent(e.target.value)}
165
+ disabled={isGenerating || isSending}
166
+ rows={8}
167
+ className="w-full text-sm font-sans leading-relaxed bg-slate-950/50 p-4 border border-slate-900 rounded-xl focus:outline-none focus:border-purple-600 resize-y text-slate-300"
168
+ placeholder={isGenerating ? "AI is typing reply word-by-word..." : "Edit response..."}
169
+ />
170
+
171
+ <div className="flex items-center justify-between mt-1">
172
+ {/* Status indicators */}
173
+ <div>
174
+ {isGenerating && (
175
+ <div className="flex items-center gap-2 text-purple-400 text-xs font-medium">
176
+ <div className="h-3 w-3 animate-spin rounded-full border border-purple-400 border-t-transparent" />
177
+ <span>Streaming draft...</span>
178
+ </div>
179
+ )}
180
+ {sendStatus === 'success' && (
181
+ <div className="flex items-center gap-1.5 text-emerald-400 text-xs font-medium">
182
+ <CheckCircle size={14} />
183
+ <span>Reply sent successfully!</span>
184
+ </div>
185
+ )}
186
+ {sendStatus === 'error' && (
187
+ <div className="flex items-center gap-1.5 text-red-400 text-xs font-medium">
188
+ <AlertCircle size={14} />
189
+ <span>{errorMsg}</span>
190
+ </div>
191
+ )}
192
+ </div>
193
+
194
+ <div className="flex gap-2">
195
+ <button
196
+ onClick={generateDraft}
197
+ disabled={isGenerating || isSending}
198
+ className="bg-slate-950 border border-slate-800 text-slate-300 hover:text-white px-4 py-2 rounded-xl text-xs font-semibold cursor-pointer transition disabled:opacity-50"
199
+ >
200
+ Regenerate
201
+ </button>
202
+
203
+ <button
204
+ onClick={handleSend}
205
+ disabled={isGenerating || isSending || !draftContent.trim()}
206
+ className="glow-btn text-white px-4.5 py-2 rounded-xl text-xs font-semibold flex items-center gap-1.5 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
207
+ >
208
+ <Send size={12} />
209
+ <span>{isSending ? 'Sending...' : 'Send Reply'}</span>
210
+ </button>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ )}
215
+ </div>
216
+ );
217
+ }
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "lucide-react": "^1.21.0",
13
+ "next": "16.2.9",
14
+ "react": "19.2.4",
15
+ "react-dom": "19.2.4"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "16.2.9",
24
+ "tailwindcss": "^4",
25
+ "typescript": "^5"
26
+ }
27
+ }
frontend/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
frontend/public/file.svg ADDED
frontend/public/globe.svg ADDED
frontend/public/next.svg ADDED
frontend/public/vercel.svg ADDED
frontend/public/window.svg ADDED
frontend/tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
grafana_dashboard.json ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "annotations": {
3
+ "list": [
4
+ {
5
+ "builtIn": 1,
6
+ "datasource": {
7
+ "type": "datasource",
8
+ "uid": "grafana"
9
+ },
10
+ "enable": true,
11
+ "hide": true,
12
+ "name": "Annotations & Alerts",
13
+ "type": "dashboard"
14
+ }
15
+ ]
16
+ },
17
+ "editable": true,
18
+ "fiscalYearStartMonth": 0,
19
+ "graphTooltip": 0,
20
+ "id": null,
21
+ "links": [],
22
+ "liveNow": false,
23
+ "panels": [
24
+ {
25
+ "collapsed": false,
26
+ "gridPos": {
27
+ "h": 8,
28
+ "w": 12,
29
+ "x": 0,
30
+ "y": 0
31
+ },
32
+ "id": 1,
33
+ "title": "API Request Rate (QPS)",
34
+ "type": "timeseries",
35
+ "datasource": {
36
+ "type": "prometheus",
37
+ "uid": "prometheus"
38
+ },
39
+ "targets": [
40
+ {
41
+ "datasource": {
42
+ "type": "prometheus",
43
+ "uid": "prometheus"
44
+ },
45
+ "editorMode": "code",
46
+ "expr": "sum(rate(http_requests_total[1m]))",
47
+ "legendFormat": "Requests per Second",
48
+ "range": true,
49
+ "refId": "A"
50
+ }
51
+ ],
52
+ "fieldConfig": {
53
+ "defaults": {
54
+ "custom": {
55
+ "drawStyle": "line",
56
+ "lineInterpolation": "smooth",
57
+ "fillOpacity": 15
58
+ },
59
+ "unit": "req/sec"
60
+ }
61
+ }
62
+ },
63
+ {
64
+ "collapsed": false,
65
+ "gridPos": {
66
+ "h": 8,
67
+ "w": 12,
68
+ "x": 12,
69
+ "y": 0
70
+ },
71
+ "id": 2,
72
+ "title": "Average Request Latency",
73
+ "type": "timeseries",
74
+ "datasource": {
75
+ "type": "prometheus",
76
+ "uid": "prometheus"
77
+ },
78
+ "targets": [
79
+ {
80
+ "datasource": {
81
+ "type": "prometheus",
82
+ "uid": "prometheus"
83
+ },
84
+ "editorMode": "code",
85
+ "expr": "sum(rate(http_request_duration_seconds_sum[5m])) / sum(rate(http_request_duration_seconds_count[5m]))",
86
+ "legendFormat": "Latency (s)",
87
+ "range": true,
88
+ "refId": "A"
89
+ }
90
+ ],
91
+ "fieldConfig": {
92
+ "defaults": {
93
+ "custom": {
94
+ "drawStyle": "line",
95
+ "lineInterpolation": "smooth"
96
+ },
97
+ "unit": "s"
98
+ }
99
+ }
100
+ },
101
+ {
102
+ "collapsed": false,
103
+ "gridPos": {
104
+ "h": 8,
105
+ "w": 12,
106
+ "x": 0,
107
+ "y": 8
108
+ },
109
+ "id": 3,
110
+ "title": "Celery Task Execution Rate",
111
+ "type": "timeseries",
112
+ "datasource": {
113
+ "type": "prometheus",
114
+ "uid": "prometheus"
115
+ },
116
+ "targets": [
117
+ {
118
+ "datasource": {
119
+ "type": "prometheus",
120
+ "uid": "prometheus"
121
+ },
122
+ "editorMode": "code",
123
+ "expr": "sum(rate(celery_task_status_count_total[5m])) by (status)",
124
+ "legendFormat": "{{status}} Tasks",
125
+ "range": true,
126
+ "refId": "A"
127
+ }
128
+ ],
129
+ "fieldConfig": {
130
+ "defaults": {
131
+ "custom": {
132
+ "drawStyle": "line"
133
+ }
134
+ }
135
+ }
136
+ },
137
+ {
138
+ "collapsed": false,
139
+ "gridPos": {
140
+ "h": 8,
141
+ "w": 12,
142
+ "x": 12,
143
+ "y": 8
144
+ },
145
+ "id": 4,
146
+ "title": "AI Agent Runs (Success / Failure)",
147
+ "type": "timeseries",
148
+ "datasource": {
149
+ "type": "prometheus",
150
+ "uid": "prometheus"
151
+ },
152
+ "targets": [
153
+ {
154
+ "datasource": {
155
+ "type": "prometheus",
156
+ "uid": "prometheus"
157
+ },
158
+ "editorMode": "code",
159
+ "expr": "sum(rate(agent_run_duration_seconds_count[5m])) by (status)",
160
+ "legendFormat": "Agent Status: {{status}}",
161
+ "range": true,
162
+ "refId": "A"
163
+ }
164
+ ],
165
+ "fieldConfig": {
166
+ "defaults": {
167
+ "custom": {
168
+ "drawStyle": "line"
169
+ }
170
+ }
171
+ }
172
+ }
173
+ ],
174
+ "schemaVersion": 38,
175
+ "style": "dark",
176
+ "tags": ["fastapi", "email-agent"],
177
+ "time": {
178
+ "from": "now-1h",
179
+ "to": "now"
180
+ },
181
+ "timepicker": {},
182
+ "timezone": "browser",
183
+ "title": "Agentic AI Email Observability Dashboard",
184
+ "version": 1
185
+ }
prometheus.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ global:
2
+ scrape_interval: 5s
3
+ evaluation_interval: 5s
4
+
5
+ scrape_configs:
6
+ - job_name: "fastapi-backend"
7
+ metrics_path: "/metrics"
8
+ static_configs:
9
+ - targets: ["host.docker.internal:8000"]