mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:21:09 +02:00
make auth working
This commit is contained in:
parent
7a1f1e9aef
commit
25824ed0ac
73 changed files with 9093 additions and 3877 deletions
File diff suppressed because it is too large
Load diff
37
.dockerignore
Normal file
37
.dockerignore
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# TypeScript build cache - must be excluded to prevent incremental build issues
|
||||||
|
**/*.tsbuildinfo
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
**/node_modules
|
||||||
|
**/.pnpm-store
|
||||||
|
|
||||||
|
# Build outputs are excluded to force fresh builds
|
||||||
|
**/dist
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
**/.env
|
||||||
|
**/.env.local
|
||||||
|
**/.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
**/.DS_Store
|
||||||
|
**/Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
**/*.log
|
||||||
|
**/npm-debug.log*
|
||||||
|
**/pnpm-debug.log*
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
**/coverage
|
||||||
|
**/.nyc_output
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
681
chat/TESTING_GUIDE.md
Normal file
681
chat/TESTING_GUIDE.md
Normal file
|
|
@ -0,0 +1,681 @@
|
||||||
|
# Testing Guide - Mana Core Auth Integration
|
||||||
|
|
||||||
|
This guide walks you through testing the Chat project with Mana Core Auth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before testing, make sure you have:
|
||||||
|
- ✅ Node.js 20+
|
||||||
|
- ✅ pnpm installed
|
||||||
|
- ✅ All dependencies installed (`pnpm install` from monorepo root)
|
||||||
|
- ✅ PostgreSQL running (or Docker for Mana Core Auth)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Generate JWT Keys for Mana Core Auth
|
||||||
|
|
||||||
|
Mana Core Auth requires RS256 JWT keys. Generate them first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mana-core-auth
|
||||||
|
chmod +x scripts/generate-keys.sh
|
||||||
|
./scripts/generate-keys.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**You'll see output like:**
|
||||||
|
```
|
||||||
|
Generating RS256 key pair...
|
||||||
|
Keys generated successfully!
|
||||||
|
|
||||||
|
Private key: private.pem
|
||||||
|
Public key: public.pem
|
||||||
|
|
||||||
|
Add these to your .env file:
|
||||||
|
|
||||||
|
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKC...
|
||||||
|
-----END RSA PRIVATE KEY-----"
|
||||||
|
|
||||||
|
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBg...
|
||||||
|
-----END PUBLIC KEY-----"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Copy these keys - you'll need them in the next step!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Configure Environment Variables
|
||||||
|
|
||||||
|
### 2.1 Mana Core Auth
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mana-core-auth
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `mana-core-auth/.env` and add:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://manacore:password@localhost:5432/manacore
|
||||||
|
|
||||||
|
# Paste the keys from Step 1
|
||||||
|
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
YOUR_PRIVATE_KEY_HERE
|
||||||
|
-----END RSA PRIVATE KEY-----"
|
||||||
|
|
||||||
|
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||||
|
YOUR_PUBLIC_KEY_HERE
|
||||||
|
-----END PUBLIC KEY-----"
|
||||||
|
|
||||||
|
# Other settings (use defaults for now)
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://localhost:8081
|
||||||
|
PORT=3001
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Chat Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../chat/backend
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `chat/backend/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Azure OpenAI (your existing keys)
|
||||||
|
AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com
|
||||||
|
AZURE_OPENAI_API_KEY=your-api-key
|
||||||
|
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||||
|
|
||||||
|
# Mana Core Auth (NEW)
|
||||||
|
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# Supabase (for database, not auth)
|
||||||
|
SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
SUPABASE_SERVICE_KEY=your-service-key
|
||||||
|
|
||||||
|
# Server
|
||||||
|
PORT=3002
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Chat Web App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../apps/web
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `chat/apps/web/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Mana Core Auth (NEW)
|
||||||
|
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# Backend API (NEW PORT)
|
||||||
|
PUBLIC_BACKEND_URL=http://localhost:3002
|
||||||
|
|
||||||
|
# Supabase (for database)
|
||||||
|
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Chat Mobile App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../mobile
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `chat/apps/mobile/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Mana Core Auth (NEW)
|
||||||
|
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# Backend API (NEW PORT)
|
||||||
|
EXPO_PUBLIC_BACKEND_URL=http://localhost:3002
|
||||||
|
|
||||||
|
# Supabase (for database)
|
||||||
|
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Start Services (4 Terminals)
|
||||||
|
|
||||||
|
### Terminal 1: Mana Core Auth
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mana-core-auth
|
||||||
|
|
||||||
|
# Start PostgreSQL (if using Docker)
|
||||||
|
docker-compose up postgres -d
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
pnpm migration:run
|
||||||
|
|
||||||
|
# Start auth service
|
||||||
|
pnpm start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
```
|
||||||
|
🚀 Mana Core Auth running on: http://localhost:3001
|
||||||
|
📚 Environment: development
|
||||||
|
```
|
||||||
|
|
||||||
|
**Leave this running!**
|
||||||
|
|
||||||
|
### Terminal 2: Chat Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd chat/backend
|
||||||
|
pnpm start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
```
|
||||||
|
[Nest] LOG [NestApplication] Nest application successfully started
|
||||||
|
[Nest] LOG Listening on port 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
**Leave this running!**
|
||||||
|
|
||||||
|
### Terminal 3: Chat Web App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd chat/apps/web
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
```
|
||||||
|
VITE v5.x.x ready in xxx ms
|
||||||
|
|
||||||
|
➜ Local: http://localhost:5173/
|
||||||
|
➜ Network: use --host to expose
|
||||||
|
```
|
||||||
|
|
||||||
|
**Leave this running!**
|
||||||
|
|
||||||
|
### Terminal 4: Chat Mobile App (Optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd chat/apps/mobile
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
```
|
||||||
|
› Metro waiting on exp://localhost:8081
|
||||||
|
› Scan the QR code above with Expo Go (Android) or Camera (iOS)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Test Web App Authentication
|
||||||
|
|
||||||
|
### 4.1 Open Web App
|
||||||
|
|
||||||
|
Open browser: http://localhost:5173
|
||||||
|
|
||||||
|
### 4.2 Register New User
|
||||||
|
|
||||||
|
1. Click **"Register"** or go to `/register`
|
||||||
|
2. Enter test credentials:
|
||||||
|
- Email: `test@example.com`
|
||||||
|
- Password: `Test1234!`
|
||||||
|
3. Click **"Register"**
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
- Registration succeeds
|
||||||
|
- Automatically redirects to login
|
||||||
|
- Login happens automatically
|
||||||
|
- You're redirected to the main app
|
||||||
|
|
||||||
|
### 4.3 Check Authentication
|
||||||
|
|
||||||
|
Open browser console (F12) and run:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check if user is authenticated
|
||||||
|
console.log('Authenticated:', window.localStorage.getItem('@auth/appToken'));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** You should see a JWT token
|
||||||
|
|
||||||
|
### 4.4 Check Credits
|
||||||
|
|
||||||
|
In browser console:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get credit balance
|
||||||
|
const authStore = window.authStore; // If exported globally
|
||||||
|
// Or navigate to a page that displays credits
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
- 150 initial credits
|
||||||
|
- API call to `/api/v1/credits/balance` succeeds
|
||||||
|
|
||||||
|
### 4.5 Test Logout
|
||||||
|
|
||||||
|
1. Click **"Logout"** button
|
||||||
|
2. Check you're redirected to login page
|
||||||
|
3. Try accessing protected route → Should redirect to login
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Test Backend API
|
||||||
|
|
||||||
|
### 5.1 Get Access Token
|
||||||
|
|
||||||
|
First, login via web app, then get the token from localStorage:
|
||||||
|
|
||||||
|
**In browser console:**
|
||||||
|
```javascript
|
||||||
|
const token = localStorage.getItem('@auth/appToken');
|
||||||
|
console.log(token);
|
||||||
|
// Copy this token!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Test Protected Endpoints
|
||||||
|
|
||||||
|
Use `curl` or Postman/Insomnia:
|
||||||
|
|
||||||
|
#### Test 1: Get AI Models (Protected)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Without token - Should fail with 401
|
||||||
|
curl http://localhost:3002/api/chat/models
|
||||||
|
|
||||||
|
# With token - Should succeed
|
||||||
|
curl http://localhost:3002/api/chat/models \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"name": "GPT-O3-Mini",
|
||||||
|
"description": "Azure OpenAI O3-Mini: Effizientes Modell für schnelle Antworten.",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test 2: List Conversations (Protected)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/api/conversations \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** Array of conversations (may be empty for new user)
|
||||||
|
|
||||||
|
#### Test 3: Create Conversation (Protected)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/conversations \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"modelId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"title": "Test Conversation"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** New conversation object
|
||||||
|
|
||||||
|
#### Test 4: Chat Completion (Protected)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/chat/completions \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"modelId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Say hello!"}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** AI response with content and usage stats
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Test Mobile App (Optional)
|
||||||
|
|
||||||
|
### 6.1 Install Expo Go
|
||||||
|
|
||||||
|
- **iOS:** Install from App Store
|
||||||
|
- **Android:** Install from Google Play Store
|
||||||
|
|
||||||
|
### 6.2 Scan QR Code
|
||||||
|
|
||||||
|
1. Look at Terminal 4 (mobile app terminal)
|
||||||
|
2. Scan the QR code with:
|
||||||
|
- iOS: Camera app
|
||||||
|
- Android: Expo Go app
|
||||||
|
|
||||||
|
### 6.3 Register/Login
|
||||||
|
|
||||||
|
1. App opens to login screen
|
||||||
|
2. Tap **"Register"**
|
||||||
|
3. Enter credentials:
|
||||||
|
- Email: `mobile@example.com`
|
||||||
|
- Password: `Mobile1234!`
|
||||||
|
4. Tap **"Register"**
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
- Registration succeeds
|
||||||
|
- Auto-login
|
||||||
|
- Redirected to chat interface
|
||||||
|
|
||||||
|
### 6.4 Test Chat
|
||||||
|
|
||||||
|
1. Try sending a message
|
||||||
|
2. Should get AI response
|
||||||
|
3. Check conversation is saved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Test Token Validation
|
||||||
|
|
||||||
|
### 7.1 Test Invalid Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/api/chat/models \
|
||||||
|
-H "Authorization: Bearer invalid-token-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 401,
|
||||||
|
"message": "Invalid token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Test Expired Token
|
||||||
|
|
||||||
|
After 15 minutes, the access token expires. Try using it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/api/chat/models \
|
||||||
|
-H "Authorization: Bearer EXPIRED_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 401 Unauthorized
|
||||||
|
|
||||||
|
### 7.3 Test Token Refresh
|
||||||
|
|
||||||
|
The `@manacore/shared-auth` package automatically refreshes tokens. To test:
|
||||||
|
|
||||||
|
1. Wait 15+ minutes (or change `JWT_ACCESS_TOKEN_EXPIRY=1m` for testing)
|
||||||
|
2. Make an API call from web/mobile app
|
||||||
|
3. Check Network tab - should see automatic refresh
|
||||||
|
4. Request succeeds with new token
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Test Credit System
|
||||||
|
|
||||||
|
### 8.1 Check Initial Balance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/v1/credits/balance \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"balance": 0,
|
||||||
|
"freeCreditsRemaining": 150,
|
||||||
|
"totalEarned": 0,
|
||||||
|
"totalSpent": 0,
|
||||||
|
"dailyFreeCredits": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Use Credits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3001/api/v1/credits/use \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"amount": 10,
|
||||||
|
"appId": "chat",
|
||||||
|
"description": "Test chat completion",
|
||||||
|
"idempotencyKey": "test-123"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"transaction": { ... },
|
||||||
|
"newBalance": {
|
||||||
|
"balance": 0,
|
||||||
|
"freeCreditsRemaining": 140,
|
||||||
|
"totalSpent": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Check Transaction History
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/v1/credits/transactions \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** Array with signup bonus and usage transactions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues & Solutions
|
||||||
|
|
||||||
|
### Issue 1: "Connection refused" to port 3001
|
||||||
|
|
||||||
|
**Problem:** Mana Core Auth not running
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
cd mana-core-auth
|
||||||
|
pnpm start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 2: "Invalid token" errors
|
||||||
|
|
||||||
|
**Problem:** JWT keys mismatch or token expired
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Clear tokens: `localStorage.clear()` in browser
|
||||||
|
2. Login again
|
||||||
|
3. Verify JWT keys are identical in Mana Core Auth .env
|
||||||
|
|
||||||
|
### Issue 3: CORS errors in browser
|
||||||
|
|
||||||
|
**Problem:** Web app URL not in CORS whitelist
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
Edit `mana-core-auth/.env`:
|
||||||
|
```env
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://localhost:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Mana Core Auth
|
||||||
|
|
||||||
|
### Issue 4: "Database connection failed"
|
||||||
|
|
||||||
|
**Problem:** PostgreSQL not running
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# If using Docker
|
||||||
|
cd mana-core-auth
|
||||||
|
docker-compose up postgres -d
|
||||||
|
|
||||||
|
# Check it's running
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 5: "Port 3001 already in use"
|
||||||
|
|
||||||
|
**Problem:** Another service using port 3001
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Find what's using the port
|
||||||
|
lsof -ti:3001
|
||||||
|
|
||||||
|
# Kill it
|
||||||
|
kill -9 $(lsof -ti:3001)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 6: Mobile app can't connect
|
||||||
|
|
||||||
|
**Problem:** Using localhost on mobile device
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
Edit `chat/apps/mobile/.env`:
|
||||||
|
```env
|
||||||
|
# Replace localhost with your computer's IP
|
||||||
|
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://192.168.1.XXX:3001
|
||||||
|
EXPO_PUBLIC_BACKEND_URL=http://192.168.1.XXX:3002
|
||||||
|
```
|
||||||
|
|
||||||
|
Get your IP:
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
ipconfig getifaddr en0
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
hostname -I
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Test Script
|
||||||
|
|
||||||
|
Save this as `test-auth.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🧪 Testing Mana Core Auth Integration"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: Register user
|
||||||
|
echo "1️⃣ Testing registration..."
|
||||||
|
REGISTER_RESPONSE=$(curl -s -X POST http://localhost:3001/api/v1/auth/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"test@example.com","password":"Test1234!"}')
|
||||||
|
|
||||||
|
echo "Response: $REGISTER_RESPONSE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: Login
|
||||||
|
echo "2️⃣ Testing login..."
|
||||||
|
LOGIN_RESPONSE=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"test@example.com","password":"Test1234!"}')
|
||||||
|
|
||||||
|
# Extract token
|
||||||
|
TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4)
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "❌ Login failed!"
|
||||||
|
echo "Response: $LOGIN_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Login successful! Token: ${TOKEN:0:50}..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: Get credits
|
||||||
|
echo "3️⃣ Testing credit balance..."
|
||||||
|
CREDITS_RESPONSE=$(curl -s http://localhost:3001/api/v1/credits/balance \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
echo "Response: $CREDITS_RESPONSE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: Backend protected endpoint
|
||||||
|
echo "4️⃣ Testing backend protected endpoint..."
|
||||||
|
MODELS_RESPONSE=$(curl -s http://localhost:3002/api/chat/models \
|
||||||
|
-H "Authorization: Bearer $TOKEN")
|
||||||
|
|
||||||
|
echo "Response: $MODELS_RESPONSE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ All tests complete!"
|
||||||
|
```
|
||||||
|
|
||||||
|
Make it executable and run:
|
||||||
|
```bash
|
||||||
|
chmod +x test-auth.sh
|
||||||
|
./test-auth.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Use this checklist to verify everything works:
|
||||||
|
|
||||||
|
### Mana Core Auth ✅
|
||||||
|
- [ ] Service starts on port 3001
|
||||||
|
- [ ] Can register new user
|
||||||
|
- [ ] Can login with credentials
|
||||||
|
- [ ] Can refresh access token
|
||||||
|
- [ ] Can logout
|
||||||
|
- [ ] Can check credit balance
|
||||||
|
- [ ] Can use credits
|
||||||
|
|
||||||
|
### Chat Backend ✅
|
||||||
|
- [ ] Service starts on port 3002
|
||||||
|
- [ ] Protected endpoints return 401 without token
|
||||||
|
- [ ] Protected endpoints work with valid token
|
||||||
|
- [ ] Can list AI models
|
||||||
|
- [ ] Can create conversation
|
||||||
|
- [ ] Can list conversations
|
||||||
|
- [ ] Can send messages
|
||||||
|
|
||||||
|
### Web App ✅
|
||||||
|
- [ ] App loads on port 5173
|
||||||
|
- [ ] Can register new user
|
||||||
|
- [ ] Can login
|
||||||
|
- [ ] Can logout
|
||||||
|
- [ ] Can access protected routes
|
||||||
|
- [ ] Can send chat messages
|
||||||
|
- [ ] Can see conversations
|
||||||
|
|
||||||
|
### Mobile App ✅
|
||||||
|
- [ ] App loads in Expo Go
|
||||||
|
- [ ] Can register new user
|
||||||
|
- [ ] Can login
|
||||||
|
- [ ] Can logout
|
||||||
|
- [ ] Can send chat messages
|
||||||
|
- [ ] Can see conversations
|
||||||
|
- [ ] Tokens persist after app restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** Ready for Testing! 🚀
|
||||||
|
|
||||||
|
Follow these steps and check off items as you test. If you encounter issues, check the "Common Issues" section above.
|
||||||
633
chat/apps/mobile/services/api.ts
Normal file
633
chat/apps/mobile/services/api.ts
Normal file
|
|
@ -0,0 +1,633 @@
|
||||||
|
/**
|
||||||
|
* API Client for Chat Mobile App
|
||||||
|
* Handles all communication with the NestJS backend
|
||||||
|
*/
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
|
// Token storage key (must match what @manacore/shared-auth uses)
|
||||||
|
const APP_TOKEN_KEY = '@manacore/app_token';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type Conversation = {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
modelId: string;
|
||||||
|
templateId?: string;
|
||||||
|
spaceId?: string;
|
||||||
|
conversationMode: 'free' | 'guided' | 'template';
|
||||||
|
documentMode: boolean;
|
||||||
|
title?: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
sender: 'user' | 'assistant' | 'system';
|
||||||
|
messageText: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Template = {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion?: string;
|
||||||
|
modelId?: string;
|
||||||
|
color: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
documentMode: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Space = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
ownerId: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpaceMember = {
|
||||||
|
id: string;
|
||||||
|
spaceId: string;
|
||||||
|
userId: string;
|
||||||
|
role: 'owner' | 'admin' | 'member' | 'viewer';
|
||||||
|
invitationStatus: 'pending' | 'accepted' | 'declined';
|
||||||
|
invitedBy?: string;
|
||||||
|
invitedAt: string;
|
||||||
|
joinedAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Document = {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
version: number;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AIModel = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
parameters: {
|
||||||
|
temperature?: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
provider?: string;
|
||||||
|
deployment?: string;
|
||||||
|
endpoint?: string;
|
||||||
|
api_version?: string;
|
||||||
|
};
|
||||||
|
costSettings?: {
|
||||||
|
prompt_per_1k_tokens?: number;
|
||||||
|
completion_per_1k_tokens?: number;
|
||||||
|
};
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatMessage = {
|
||||||
|
role: 'system' | 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TokenUsage = {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatCompletionResponse = {
|
||||||
|
content: string;
|
||||||
|
usage: TokenUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Base API Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function getAuthToken(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
return await SecureStore.getItemAsync(APP_TOKEN_KEY);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiRequest<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<{ data: T | null; error: string | null }> {
|
||||||
|
try {
|
||||||
|
const token = await getAuthToken();
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(options.headers || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${BACKEND_URL}${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`API Error [${response.status}]: ${errorText}`);
|
||||||
|
return { data: null, error: `API Error: ${response.status}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty responses
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) {
|
||||||
|
return { data: null, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
return { data, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Request failed:', error);
|
||||||
|
return { data: null, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Conversation API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const conversationApi = {
|
||||||
|
async getConversations(spaceId?: string): Promise<Conversation[]> {
|
||||||
|
const params = spaceId ? `?spaceId=${spaceId}` : '';
|
||||||
|
const { data, error } = await apiRequest<Conversation[]>(`/api/conversations${params}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch conversations:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getArchivedConversations(): Promise<Conversation[]> {
|
||||||
|
const { data, error } = await apiRequest<Conversation[]>('/api/conversations/archived');
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch archived conversations:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getConversation(id: string): Promise<Conversation | null> {
|
||||||
|
const { data, error } = await apiRequest<Conversation>(`/api/conversations/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch conversation:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getMessages(conversationId: string): Promise<Message[]> {
|
||||||
|
const { data, error } = await apiRequest<Message[]>(`/api/conversations/${conversationId}/messages`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch messages:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async createConversation(params: {
|
||||||
|
modelId: string;
|
||||||
|
conversationMode?: 'free' | 'guided' | 'template';
|
||||||
|
templateId?: string;
|
||||||
|
documentMode?: boolean;
|
||||||
|
spaceId?: string;
|
||||||
|
}): Promise<Conversation | null> {
|
||||||
|
const { data, error } = await apiRequest<Conversation>('/api/conversations', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create conversation:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async addMessage(
|
||||||
|
conversationId: string,
|
||||||
|
sender: 'user' | 'assistant' | 'system',
|
||||||
|
messageText: string
|
||||||
|
): Promise<Message | null> {
|
||||||
|
const { data, error } = await apiRequest<Message>(`/api/conversations/${conversationId}/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ sender, messageText }),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to add message:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/conversations/${conversationId}/title`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ title }),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async archiveConversation(conversationId: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/conversations/${conversationId}/archive`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/conversations/${conversationId}/unarchive`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteConversation(conversationId: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/conversations/${conversationId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Template API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const templateApi = {
|
||||||
|
async getTemplates(): Promise<Template[]> {
|
||||||
|
const { data, error } = await apiRequest<Template[]>('/api/templates');
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch templates:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplate(id: string): Promise<Template | null> {
|
||||||
|
const { data, error } = await apiRequest<Template>(`/api/templates/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch template:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDefaultTemplate(): Promise<Template | null> {
|
||||||
|
const { data, error } = await apiRequest<Template>('/api/templates/default');
|
||||||
|
if (error) {
|
||||||
|
// Not finding a default template is not an error
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTemplate(template: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion?: string;
|
||||||
|
modelId?: string;
|
||||||
|
color?: string;
|
||||||
|
documentMode?: boolean;
|
||||||
|
}): Promise<Template | null> {
|
||||||
|
const { data, error } = await apiRequest<Template>('/api/templates', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(template),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create template:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTemplate(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion: string;
|
||||||
|
modelId: string;
|
||||||
|
color: string;
|
||||||
|
documentMode: boolean;
|
||||||
|
}>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/templates/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setDefaultTemplate(id: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/templates/${id}/default`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/templates/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Space API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const spaceApi = {
|
||||||
|
async getUserSpaces(): Promise<Space[]> {
|
||||||
|
const { data, error } = await apiRequest<Space[]>('/api/spaces');
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch spaces:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOwnedSpaces(): Promise<Space[]> {
|
||||||
|
const { data, error } = await apiRequest<Space[]>('/api/spaces/owned');
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch owned spaces:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSpace(id: string): Promise<Space | null> {
|
||||||
|
const { data, error } = await apiRequest<Space>(`/api/spaces/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch space:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||||
|
const { data, error } = await apiRequest<SpaceMember[]>(`/api/spaces/${spaceId}/members`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch space members:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserRoleInSpace(spaceId: string): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||||
|
const { data, error } = await apiRequest<{ role: 'owner' | 'admin' | 'member' | 'viewer' }>(`/api/spaces/${spaceId}/role`);
|
||||||
|
if (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data?.role || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPendingInvitations(): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||||
|
const { data, error } = await apiRequest<Array<{ invitation: SpaceMember; space: Space }>>('/api/spaces/invitations/pending');
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch pending invitations:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async createSpace(name: string, description?: string): Promise<Space | null> {
|
||||||
|
const { data, error } = await apiRequest<Space>('/api/spaces', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name, description }),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create space:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateSpace(
|
||||||
|
id: string,
|
||||||
|
updates: { name?: string; description?: string; isArchived?: boolean }
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/spaces/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteSpace(id: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/spaces/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async inviteUser(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
role: 'admin' | 'member' | 'viewer' = 'member'
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/members`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ userId, role }),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async respondToInvitation(
|
||||||
|
spaceId: string,
|
||||||
|
status: 'accepted' | 'declined'
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/invitation`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/members/${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
|
||||||
|
async changeMemberRole(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
newRole: 'admin' | 'member' | 'viewer'
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/members/${userId}/role`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ role: newRole }),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Document API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const documentApi = {
|
||||||
|
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||||
|
const { data, error } = await apiRequest<Document>(`/api/documents/conversation/${conversationId}/latest`);
|
||||||
|
if (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||||
|
const { data, error } = await apiRequest<Document[]>(`/api/documents/conversation/${conversationId}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch document versions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async hasDocument(conversationId: string): Promise<boolean> {
|
||||||
|
const { data, error } = await apiRequest<{ exists: boolean }>(`/api/documents/conversation/${conversationId}/exists`);
|
||||||
|
if (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return data?.exists || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
||||||
|
const { data, error } = await apiRequest<Document>('/api/documents', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ conversationId, content }),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create document:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDocumentVersion(conversationId: string, content: string): Promise<Document | null> {
|
||||||
|
const { data, error } = await apiRequest<Document>(`/api/documents/conversation/${conversationId}/version`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ content }),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create document version:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest(`/api/documents/${documentId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Model API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const modelApi = {
|
||||||
|
async getModels(): Promise<AIModel[]> {
|
||||||
|
const { data, error } = await apiRequest<AIModel[]>('/api/chat/models');
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch models:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getModel(id: string): Promise<AIModel | null> {
|
||||||
|
const { data, error } = await apiRequest<AIModel>(`/api/models/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch model:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Chat API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const chatApi = {
|
||||||
|
async createCompletion(params: {
|
||||||
|
messages: ChatMessage[];
|
||||||
|
modelId: string;
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
}): Promise<ChatCompletionResponse | null> {
|
||||||
|
const { data, error } = await apiRequest<ChatCompletionResponse>('/api/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: params.messages,
|
||||||
|
modelId: params.modelId,
|
||||||
|
temperature: params.temperature ?? 0.7,
|
||||||
|
maxTokens: params.maxTokens ?? 1000,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to create completion:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Usage Log API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const usageApi = {
|
||||||
|
async logTokenUsage(params: {
|
||||||
|
conversationId: string;
|
||||||
|
messageId: string;
|
||||||
|
modelId: string;
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
estimatedCost: number;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const { error } = await apiRequest('/api/usage-logs', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
return !error;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
import { supabase } from '../utils/supabase';
|
/**
|
||||||
import { sendChatRequest, ChatMessage, logTokenUsage, ChatRequestResult } from './openai';
|
* Conversation Service - CRUD operations via Backend API
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
conversationApi,
|
||||||
|
chatApi,
|
||||||
|
modelApi,
|
||||||
|
templateApi,
|
||||||
|
usageApi,
|
||||||
|
type Conversation as ApiConversation,
|
||||||
|
type Message as ApiMessage,
|
||||||
|
type ChatMessage,
|
||||||
|
type TokenUsage,
|
||||||
|
} from './api';
|
||||||
|
|
||||||
// Typdefinitionen für Konversationen und Nachrichten
|
// Re-export types with backwards-compatible naming (snake_case for mobile)
|
||||||
export type Conversation = {
|
export type Conversation = {
|
||||||
id: string; // UUID
|
id: string;
|
||||||
user_id: string; // UUID des Benutzers (auth.uid)
|
user_id: string;
|
||||||
model_id: string; // UUID
|
model_id: string;
|
||||||
template_id?: string; // UUID, optional
|
template_id?: string;
|
||||||
|
space_id?: string;
|
||||||
conversation_mode: 'free' | 'guided' | 'template';
|
conversation_mode: 'free' | 'guided' | 'template';
|
||||||
document_mode: boolean; // Gibt an, ob der Dokumentmodus aktiviert ist
|
document_mode: boolean;
|
||||||
title?: string; // Titel der Konversation
|
title?: string;
|
||||||
is_archived: boolean; // Gibt an, ob die Konversation archiviert wurde
|
is_archived: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
@ -37,6 +50,34 @@ export type TokenUsageType = {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to convert API response to local format
|
||||||
|
function toLocalConversation(conv: ApiConversation): Conversation {
|
||||||
|
return {
|
||||||
|
id: conv.id,
|
||||||
|
user_id: conv.userId,
|
||||||
|
model_id: conv.modelId,
|
||||||
|
template_id: conv.templateId,
|
||||||
|
space_id: conv.spaceId,
|
||||||
|
conversation_mode: conv.conversationMode,
|
||||||
|
document_mode: conv.documentMode,
|
||||||
|
title: conv.title,
|
||||||
|
is_archived: conv.isArchived,
|
||||||
|
created_at: conv.createdAt,
|
||||||
|
updated_at: conv.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalMessage(msg: ApiMessage): Message {
|
||||||
|
return {
|
||||||
|
id: msg.id,
|
||||||
|
conversation_id: msg.conversationId,
|
||||||
|
sender: msg.sender,
|
||||||
|
message_text: msg.messageText,
|
||||||
|
created_at: msg.createdAt,
|
||||||
|
updated_at: msg.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt eine neue Konversation in der Datenbank
|
* Erstellt eine neue Konversation in der Datenbank
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,28 +90,22 @@ export async function createConversation(
|
||||||
spaceId?: string
|
spaceId?: string
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
console.log("🔵 Erstelle Konversation mit Space ID:", spaceId || "keine");
|
console.log('🔵 Erstelle Konversation mit Space ID:', spaceId || 'keine');
|
||||||
|
|
||||||
// Erstelle einen neuen Eintrag in der Conversations-Tabelle
|
const conversation = await conversationApi.createConversation({
|
||||||
const { data, error } = await supabase
|
modelId,
|
||||||
.from('conversations')
|
conversationMode: mode,
|
||||||
.insert({
|
templateId,
|
||||||
user_id: userId,
|
documentMode,
|
||||||
model_id: modelId,
|
spaceId,
|
||||||
template_id: templateId,
|
});
|
||||||
conversation_mode: mode,
|
|
||||||
document_mode: documentMode,
|
|
||||||
space_id: spaceId,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!conversation) {
|
||||||
console.error('Fehler beim Erstellen der Konversation:', error);
|
console.error('Fehler beim Erstellen der Konversation');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.id;
|
return conversation.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Erstellen der Konversation:', error);
|
console.error('Fehler beim Erstellen der Konversation:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -86,35 +121,23 @@ export async function addMessage(
|
||||||
messageText: string
|
messageText: string
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
// Führe eine Prüfung und Validierung des Senders durch
|
// Validate sender
|
||||||
let validSender = sender;
|
let validSender = sender;
|
||||||
|
|
||||||
// Stelle sicher, dass der Sender den zulässigen Werten entspricht
|
|
||||||
// Das scheint das Problem zu sein - die Datenbank akzeptiert nur bestimmte Werte
|
|
||||||
if (!['user', 'assistant', 'system'].includes(validSender)) {
|
if (!['user', 'assistant', 'system'].includes(validSender)) {
|
||||||
console.error('Ungültiger Sender-Wert:', sender);
|
console.error('Ungültiger Sender-Wert:', sender);
|
||||||
validSender = 'user'; // Fallback auf 'user'
|
validSender = 'user';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Füge Nachricht hinzu mit Sender:', validSender);
|
console.log('Füge Nachricht hinzu mit Sender:', validSender);
|
||||||
|
|
||||||
// Füge eine neue Nachricht in die Messages-Tabelle ein
|
const message = await conversationApi.addMessage(conversationId, validSender, messageText);
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('messages')
|
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
sender: validSender,
|
|
||||||
message_text: messageText,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!message) {
|
||||||
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
console.error('Fehler beim Hinzufügen der Nachricht');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.id;
|
return message.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -126,18 +149,8 @@ export async function addMessage(
|
||||||
*/
|
*/
|
||||||
export async function getMessages(conversationId: string): Promise<Message[]> {
|
export async function getMessages(conversationId: string): Promise<Message[]> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const messages = await conversationApi.getMessages(conversationId);
|
||||||
.from('messages')
|
return messages.map(toLocalMessage);
|
||||||
.select('*')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('created_at', { ascending: true });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Laden der Nachrichten:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Message[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Nachrichten:', error);
|
console.error('Fehler beim Laden der Nachrichten:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -146,43 +159,33 @@ export async function getMessages(conversationId: string): Promise<Message[]> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generiert einen Titel für die Konversation basierend auf der ersten Benutzeranfrage
|
* Generiert einen Titel für die Konversation basierend auf der ersten Benutzeranfrage
|
||||||
* @param userQuestion Die erste Frage des Benutzers
|
|
||||||
* @returns Generierter Titel
|
|
||||||
*/
|
*/
|
||||||
export async function generateConversationTitle(userQuestion: string): Promise<string> {
|
export async function generateConversationTitle(userQuestion: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
console.log('Generiere Titel für Konversation basierend auf:',
|
console.log(
|
||||||
userQuestion.substring(0, 50) + (userQuestion.length > 50 ? '...' : ''));
|
'Generiere Titel für Konversation basierend auf:',
|
||||||
|
userQuestion.substring(0, 50) + (userQuestion.length > 50 ? '...' : '')
|
||||||
|
);
|
||||||
|
|
||||||
// Verwende speziell GPT-4o-Mini für die Titelerstellung
|
|
||||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für unseren Chat mit dieser Frage: "${userQuestion}"`;
|
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für unseren Chat mit dieser Frage: "${userQuestion}"`;
|
||||||
|
|
||||||
// Manuell 4o-mini-Modell festlegen
|
const response = await chatApi.createCompletion({
|
||||||
const chatMessages: ChatMessage[] = [
|
messages: [{ role: 'user', content: titlePrompt }],
|
||||||
{
|
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
||||||
role: 'system',
|
temperature: 0.3,
|
||||||
content: 'MODEL:gpt-4o-mini-se'
|
maxTokens: 50,
|
||||||
},
|
});
|
||||||
{
|
|
||||||
role: 'user',
|
if (!response) {
|
||||||
content: titlePrompt
|
return 'Neue Konversation';
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
|
||||||
// Sende die Anfrage mit niedrigerer Temperatur für zuverlässigere Ergebnisse
|
// Clean up title
|
||||||
const titleResponse = await sendChatRequest(chatMessages, 0.3, 50);
|
let cleanTitle = response.content
|
||||||
|
.trim()
|
||||||
|
.replace(/^["']|["']$/g, '')
|
||||||
|
.replace(/\.$/g, '');
|
||||||
|
|
||||||
// Extrahiere den Text aus der Antwort
|
|
||||||
const responseText = typeof titleResponse === 'string'
|
|
||||||
? titleResponse
|
|
||||||
: titleResponse.content;
|
|
||||||
|
|
||||||
// Entferne Anführungszeichen und Punkt am Ende, falls vorhanden
|
|
||||||
let cleanTitle = responseText.trim()
|
|
||||||
.replace(/^["']|["']$/g, '') // Entferne Anführungszeichen am Anfang und Ende
|
|
||||||
.replace(/\.$/g, ''); // Entferne Punkt am Ende
|
|
||||||
|
|
||||||
// Begrenze die Länge auf 100 Zeichen (für Datenbank)
|
|
||||||
if (cleanTitle.length > 100) {
|
if (cleanTitle.length > 100) {
|
||||||
cleanTitle = cleanTitle.substring(0, 97) + '...';
|
cleanTitle = cleanTitle.substring(0, 97) + '...';
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +194,6 @@ export async function generateConversationTitle(userQuestion: string): Promise<s
|
||||||
return cleanTitle;
|
return cleanTitle;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei der Titelgenerierung:', error);
|
console.error('Fehler bei der Titelgenerierung:', error);
|
||||||
// Fallback-Titel bei Fehler
|
|
||||||
return 'Neue Konversation';
|
return 'Neue Konversation';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,15 +201,15 @@ export async function generateConversationTitle(userQuestion: string): Promise<s
|
||||||
/**
|
/**
|
||||||
* Aktualisiert den Titel einer Konversation
|
* Aktualisiert den Titel einer Konversation
|
||||||
*/
|
*/
|
||||||
export async function updateConversationTitle(conversationId: string, title: string): Promise<boolean> {
|
export async function updateConversationTitle(
|
||||||
|
conversationId: string,
|
||||||
|
title: string
|
||||||
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
const success = await conversationApi.updateTitle(conversationId, title);
|
||||||
.from('conversations')
|
|
||||||
.update({ title })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
if (!success) {
|
||||||
console.error('Fehler beim Aktualisieren des Konversationstitels:', error);
|
console.error('Fehler beim Aktualisieren des Konversationstitels');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,23 +222,17 @@ export async function updateConversationTitle(conversationId: string, title: str
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt einen System-Prompt aus einer Vorlage
|
* Lädt einen System-Prompt aus einer Vorlage
|
||||||
* @param templateId Die ID der Vorlage
|
|
||||||
* @returns Der System-Prompt der Vorlage oder null
|
|
||||||
*/
|
*/
|
||||||
export async function getSystemPromptFromTemplate(templateId: string): Promise<string | null> {
|
export async function getSystemPromptFromTemplate(templateId: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const template = await templateApi.getTemplate(templateId);
|
||||||
.from('templates')
|
|
||||||
.select('system_prompt')
|
|
||||||
.eq('id', templateId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!template) {
|
||||||
console.error('Fehler beim Laden der Vorlage:', error);
|
console.error('Fehler beim Laden der Vorlage');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.system_prompt;
|
return template.systemPrompt;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Vorlage:', error);
|
console.error('Fehler beim Laden der Vorlage:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -252,70 +248,52 @@ export async function sendMessageAndGetResponse(
|
||||||
modelId: string,
|
modelId: string,
|
||||||
templateId?: string,
|
templateId?: string,
|
||||||
documentMode: boolean = false
|
documentMode: boolean = false
|
||||||
): Promise<{ userMessageId: string | null; assistantMessageId: string | null; assistantResponse: string; title?: string; documentContent?: string }> {
|
): Promise<{
|
||||||
|
userMessageId: string | null;
|
||||||
|
assistantMessageId: string | null;
|
||||||
|
assistantResponse: string;
|
||||||
|
title?: string;
|
||||||
|
documentContent?: string;
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
console.log('Starte sendMessageAndGetResponse mit:', {
|
console.log('Starte sendMessageAndGetResponse mit:', {
|
||||||
conversationId,
|
conversationId,
|
||||||
userMessage: userMessage.substring(0, 50) + (userMessage.length > 50 ? '...' : ''),
|
userMessage: userMessage.substring(0, 50) + (userMessage.length > 50 ? '...' : ''),
|
||||||
modelId
|
modelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lade das Modell aus der Datenbank oder verwende Fallback
|
// Load the model from API
|
||||||
const { data: modelData, error: modelError } = await supabase
|
const modelData = await modelApi.getModel(modelId);
|
||||||
.from('models')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', modelId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (modelError) {
|
if (!modelData) {
|
||||||
console.error('Fehler beim Laden des Modells:', modelError);
|
|
||||||
console.log('Verwende Standard-Parameter, da Modell nicht geladen werden konnte');
|
console.log('Verwende Standard-Parameter, da Modell nicht geladen werden konnte');
|
||||||
} else {
|
} else {
|
||||||
console.log('Modell geladen:', {
|
console.log('Modell geladen:', {
|
||||||
id: modelData.id,
|
id: modelData.id,
|
||||||
name: modelData.name,
|
name: modelData.name,
|
||||||
parameters: modelData.parameters,
|
parameters: modelData.parameters,
|
||||||
deployment: modelData.parameters?.deployment
|
deployment: modelData.parameters?.deployment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variable für die Benutzer-Nachricht-ID
|
// Save the user message
|
||||||
let userMessageId: string | null = null;
|
const userMessageId = await addMessage(conversationId, 'user', userMessage);
|
||||||
|
console.log('Benutzernachricht gespeichert mit ID:', userMessageId);
|
||||||
|
|
||||||
// Überprüfe, ob die Nachricht bereits in der Datenbank existiert
|
// Load all messages for context
|
||||||
const { data: existingMessages } = await supabase
|
|
||||||
.from('messages')
|
|
||||||
.select('id')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.eq('sender', 'user')
|
|
||||||
.eq('message_text', userMessage)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMessages && existingMessages.length > 0) {
|
|
||||||
// Wenn die Nachricht bereits existiert, verwende diese ID
|
|
||||||
userMessageId = existingMessages[0].id;
|
|
||||||
console.log('Bestehende Benutzernachricht gefunden mit ID:', userMessageId);
|
|
||||||
} else {
|
|
||||||
// Speichere die Benutzernachricht nur, wenn sie nicht bereits existiert
|
|
||||||
userMessageId = await addMessage(conversationId, 'user', userMessage);
|
|
||||||
console.log('Neue Benutzernachricht gespeichert mit ID:', userMessageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lade alle bisherigen Nachrichten für Kontext
|
|
||||||
const messages = await getMessages(conversationId);
|
const messages = await getMessages(conversationId);
|
||||||
console.log(`${messages.length} Nachrichten für Kontext geladen`);
|
console.log(`${messages.length} Nachrichten für Kontext geladen`);
|
||||||
|
|
||||||
// Konvertiere die Nachrichten in das Format für die OpenAI-API
|
// Build chat messages for API
|
||||||
const chatMessages: ChatMessage[] = [];
|
const chatMessages: ChatMessage[] = [];
|
||||||
|
|
||||||
// Lade den System-Prompt aus der Vorlage, falls template_id vorhanden
|
// Load system prompt from template if available
|
||||||
let systemPrompt: string | null = null;
|
let systemPrompt: string | null = null;
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
systemPrompt = await getSystemPromptFromTemplate(templateId);
|
systemPrompt = await getSystemPromptFromTemplate(templateId);
|
||||||
if (systemPrompt) {
|
if (systemPrompt) {
|
||||||
console.log('System-Prompt aus Vorlage geladen');
|
console.log('System-Prompt aus Vorlage geladen');
|
||||||
|
|
||||||
// Wenn Dokumentmodus aktiv ist, füge spezielle Anweisungen hinzu
|
|
||||||
if (documentMode) {
|
if (documentMode) {
|
||||||
const documentModePrompt = `
|
const documentModePrompt = `
|
||||||
${systemPrompt}
|
${systemPrompt}
|
||||||
|
|
@ -333,23 +311,13 @@ CHAT: Hier antwortest du auf die Frage oder das Feedback des Nutzers.
|
||||||
DOKUMENT:
|
DOKUMENT:
|
||||||
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
||||||
`;
|
`;
|
||||||
// Ersetze den Original-Prompt durch den Dokumentmodus-Prompt
|
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||||
chatMessages.push({
|
|
||||||
role: 'system',
|
|
||||||
content: documentModePrompt
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Dokumentmodus-Prompt hinzugefügt');
|
console.log('Dokumentmodus-Prompt hinzugefügt');
|
||||||
} else {
|
} else {
|
||||||
// Standard-Prompt ohne Dokumentmodus
|
chatMessages.push({ role: 'system', content: systemPrompt });
|
||||||
chatMessages.push({
|
|
||||||
role: 'system',
|
|
||||||
content: systemPrompt
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (documentMode) {
|
} else if (documentMode) {
|
||||||
// Wenn kein Template, aber Dokumentmodus aktiv ist
|
|
||||||
const documentModePrompt = `
|
const documentModePrompt = `
|
||||||
Du befindest dich im Dokumentmodus. Deine Aufgabe ist es, dem Benutzer zu helfen, ein Dokument zu erstellen und zu verbessern.
|
Du befindest dich im Dokumentmodus. Deine Aufgabe ist es, dem Benutzer zu helfen, ein Dokument zu erstellen und zu verbessern.
|
||||||
|
|
||||||
|
|
@ -364,102 +332,66 @@ CHAT: Hier antwortest du auf die Frage oder das Feedback des Nutzers.
|
||||||
DOKUMENT:
|
DOKUMENT:
|
||||||
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
||||||
`;
|
`;
|
||||||
chatMessages.push({
|
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||||
role: 'system',
|
|
||||||
content: documentModePrompt
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Standard-Dokumentmodus-Prompt hinzugefügt');
|
console.log('Standard-Dokumentmodus-Prompt hinzugefügt');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Füge eine System-Nachricht mit der Modell-ID hinzu, falls ein Modell geladen wurde
|
// Add all conversation messages
|
||||||
if (modelData && modelData.parameters && modelData.parameters.deployment) {
|
chatMessages.push(
|
||||||
console.log(`Nutze deployment '${modelData.parameters.deployment}' für Modell ${modelData.name}`);
|
...messages.map((msg) => ({
|
||||||
// Stelle die Modell-Identifikation ganz am Anfang ein
|
|
||||||
chatMessages.unshift({
|
|
||||||
role: 'system',
|
|
||||||
content: `MODEL:${modelData.parameters.deployment}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Versuche, das Deployment über die Modell-ID zu finden
|
|
||||||
console.warn('Kein Modell-Deployment in Modell-Daten gefunden, suche in verfügbaren Modellen');
|
|
||||||
// Lade dynamisch die Modelle
|
|
||||||
try {
|
|
||||||
const { data: availableModels } = await supabase
|
|
||||||
.from('models')
|
|
||||||
.select('id, parameters, name');
|
|
||||||
|
|
||||||
const matchingModel = availableModels?.find(m => m.id === modelId);
|
|
||||||
|
|
||||||
if (matchingModel && matchingModel.parameters && matchingModel.parameters.deployment) {
|
|
||||||
console.log(`Nutze deployment '${matchingModel.parameters.deployment}' für Modell ${matchingModel.name}`);
|
|
||||||
chatMessages.unshift({
|
|
||||||
role: 'system',
|
|
||||||
content: `MODEL:${matchingModel.parameters.deployment}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('Kein passendes Modell-Deployment gefunden, verwende Standard-Deployment');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der verfügbaren Modelle:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Füge alle normalen Nachrichten hinzu
|
|
||||||
chatMessages.push(...messages.map(msg => ({
|
|
||||||
role: msg.sender === 'user' ? 'user' : msg.sender === 'assistant' ? 'assistant' : 'system',
|
role: msg.sender === 'user' ? 'user' : msg.sender === 'assistant' ? 'assistant' : 'system',
|
||||||
content: msg.message_text
|
content: msg.message_text,
|
||||||
})));
|
})) as ChatMessage[]
|
||||||
|
);
|
||||||
|
|
||||||
console.log('Nachrichten für OpenAI konvertiert:', chatMessages.length, 'Nachrichten');
|
console.log('Nachrichten für OpenAI konvertiert:', chatMessages.length, 'Nachrichten');
|
||||||
|
|
||||||
// Sende die Anfrage an das LLM-Modell
|
// Send request to AI
|
||||||
console.log('Sende Anfrage an LLM-Modell mit Parametern:', {
|
console.log('Sende Anfrage an LLM-Modell mit Parametern:', {
|
||||||
temperature: modelData?.parameters?.temperature || 0.7,
|
temperature: modelData?.parameters?.temperature || 0.7,
|
||||||
maxTokens: modelData?.parameters?.max_tokens || 800
|
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await sendChatRequest(
|
const result = await chatApi.createCompletion({
|
||||||
chatMessages,
|
messages: chatMessages,
|
||||||
modelData?.parameters?.temperature || 0.7,
|
modelId,
|
||||||
modelData?.parameters?.max_tokens || 800
|
temperature: modelData?.parameters?.temperature || 0.7,
|
||||||
);
|
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||||
|
});
|
||||||
|
|
||||||
// Extrahiere die Antwort und Token-Nutzung aus dem Ergebnis
|
|
||||||
let assistantResponse: string;
|
let assistantResponse: string;
|
||||||
let tokenUsage;
|
let tokenUsage: TokenUsage | undefined;
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
if (!result) {
|
||||||
// Falls nur ein String zurückgegeben wurde (Fehlerfall)
|
assistantResponse =
|
||||||
assistantResponse = result;
|
'Es konnte keine Antwort generiert werden. Bitte stelle sicher, dass das Backend läuft.';
|
||||||
console.log('Einfache String-Antwort vom LLM-Modell erhalten (kein Tokennutzungs-Tracking):', {
|
|
||||||
length: assistantResponse.length,
|
|
||||||
preview: assistantResponse.substring(0, 50) + (assistantResponse.length > 50 ? '...' : '')
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Bei vollständigem Ergebnis mit Token-Nutzung
|
|
||||||
assistantResponse = result.content;
|
assistantResponse = result.content;
|
||||||
tokenUsage = result.usage;
|
tokenUsage = result.usage;
|
||||||
|
|
||||||
console.log('Antwort vom LLM-Modell erhalten:', {
|
console.log('Antwort vom LLM-Modell erhalten:', {
|
||||||
length: assistantResponse.length,
|
length: assistantResponse.length,
|
||||||
preview: assistantResponse.substring(0, 50) + (assistantResponse.length > 50 ? '...' : ''),
|
preview: assistantResponse.substring(0, 50) + (assistantResponse.length > 50 ? '...' : ''),
|
||||||
tokenUsage
|
tokenUsage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dokumentinhalt extrahieren, wenn im Dokumentmodus
|
// Extract document content if in document mode
|
||||||
let documentContent: string | undefined;
|
let documentContent: string | undefined;
|
||||||
let chatResponse = assistantResponse;
|
let chatResponse = assistantResponse;
|
||||||
|
|
||||||
if (documentMode) {
|
if (documentMode) {
|
||||||
// Nach dem Format "CHAT: ... DOKUMENT: ```markdown ... ```" suchen
|
console.log(
|
||||||
console.log("Analysiere LLM-Antwort für Dokumentextraktion:", assistantResponse.substring(0, 200) + "...");
|
'Analysiere LLM-Antwort für Dokumentextraktion:',
|
||||||
|
assistantResponse.substring(0, 200) + '...'
|
||||||
|
);
|
||||||
|
|
||||||
const chatMatch = assistantResponse.match(/CHAT:(.*?)(?=DOKUMENT:|$)/s);
|
const chatMatch = assistantResponse.match(/CHAT:(.*?)(?=DOKUMENT:|$)/s);
|
||||||
const documentMatch = assistantResponse.match(/DOKUMENT:[\s\n]*(```markdown|```|`markdown)?([^`].*?)(?:```|`+)?$/s);
|
const documentMatch = assistantResponse.match(
|
||||||
|
/DOKUMENT:[\s\n]*(```markdown|```|`markdown)?([^`].*?)(?:```|`+)?$/s
|
||||||
|
);
|
||||||
|
|
||||||
console.log("Dokument-Regex Match:", documentMatch ? "Ja" : "Nein");
|
console.log('Dokument-Regex Match:', documentMatch ? 'Ja' : 'Nein');
|
||||||
|
|
||||||
if (chatMatch && chatMatch[1]) {
|
if (chatMatch && chatMatch[1]) {
|
||||||
chatResponse = chatMatch[1].trim();
|
chatResponse = chatMatch[1].trim();
|
||||||
|
|
@ -468,121 +400,100 @@ Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-M
|
||||||
|
|
||||||
if (documentMatch && documentMatch[2]) {
|
if (documentMatch && documentMatch[2]) {
|
||||||
documentContent = documentMatch[2].trim();
|
documentContent = documentMatch[2].trim();
|
||||||
console.log('Dokument-Inhalt extrahiert:', (documentContent?.substring(0, 50) || '') + '...');
|
console.log(
|
||||||
} else if (documentMatch) {
|
'Dokument-Inhalt extrahiert:',
|
||||||
console.log('Dokument-Match gefunden, aber kein Inhalt in Gruppe 2');
|
(documentContent?.substring(0, 50) || '') + '...'
|
||||||
console.log('Dokument-Match-Gruppen:', documentMatch.length);
|
);
|
||||||
for (let i = 0; i < documentMatch.length; i++) {
|
|
||||||
console.log(`Gruppe ${i}:`, documentMatch[i]?.substring(0, 30));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speichere die Antwort des Assistenten
|
// Save assistant message
|
||||||
const assistantMessageId = await addMessage(conversationId, 'assistant', chatResponse);
|
const assistantMessageId = await addMessage(conversationId, 'assistant', chatResponse);
|
||||||
console.log('Assistentenantwort gespeichert mit ID:', assistantMessageId);
|
console.log('Assistentenantwort gespeichert mit ID:', assistantMessageId);
|
||||||
|
|
||||||
// Token-Nutzung loggen, falls verfügbar
|
// Log token usage if available
|
||||||
if (tokenUsage && assistantMessageId && userMessageId && modelData) {
|
if (tokenUsage && assistantMessageId && userMessageId) {
|
||||||
try {
|
try {
|
||||||
// Lade die Konversation, um die user_id zu erhalten
|
const estimatedCost = calculateTokenCost(
|
||||||
const { data: conversationData, error: convError } = await supabase
|
tokenUsage.prompt_tokens,
|
||||||
.from('conversations')
|
tokenUsage.completion_tokens,
|
||||||
.select('user_id')
|
modelData?.costSettings
|
||||||
.eq('id', conversationId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (convError || !conversationData) {
|
|
||||||
console.error('Fehler beim Laden der Konversation für Token-Logging:', convError);
|
|
||||||
} else {
|
|
||||||
const userId = conversationData.user_id;
|
|
||||||
|
|
||||||
// Logge die Token-Nutzung
|
|
||||||
await logTokenUsage(
|
|
||||||
tokenUsage,
|
|
||||||
conversationId,
|
|
||||||
assistantMessageId,
|
|
||||||
userId,
|
|
||||||
modelId
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await usageApi.logTokenUsage({
|
||||||
|
conversationId,
|
||||||
|
messageId: assistantMessageId,
|
||||||
|
modelId,
|
||||||
|
promptTokens: tokenUsage.prompt_tokens,
|
||||||
|
completionTokens: tokenUsage.completion_tokens,
|
||||||
|
totalTokens: tokenUsage.total_tokens,
|
||||||
|
estimatedCost,
|
||||||
|
});
|
||||||
console.log('Token-Nutzung erfolgreich geloggt');
|
console.log('Token-Nutzung erfolgreich geloggt');
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
||||||
// Wir werfen keinen Fehler, da das Token-Logging nicht kritisch ist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe, ob dies die erste Nachricht in der Konversation ist
|
// Generate title for new conversations
|
||||||
// Wenn ja, generiere einen Titel und aktualisiere die Konversation
|
const allMessages = await getMessages(conversationId);
|
||||||
const { count } = await supabase
|
let title: string | undefined;
|
||||||
.from('messages')
|
|
||||||
.select('id', { count: 'exact', head: true })
|
|
||||||
.eq('conversation_id', conversationId);
|
|
||||||
|
|
||||||
let title;
|
if (allMessages.length <= 2) {
|
||||||
// Nur für die erste oder zweite Nachricht (die erste könnte eine System-Nachricht sein)
|
|
||||||
if (count === 1 || count === 2) {
|
|
||||||
// Generiere einen Titel basierend auf der Benutzernachricht
|
|
||||||
title = await generateConversationTitle(userMessage);
|
title = await generateConversationTitle(userMessage);
|
||||||
|
|
||||||
// Aktualisiere den Titel in der Datenbank
|
if (title) {
|
||||||
const success = await updateConversationTitle(conversationId, title);
|
const success = await updateConversationTitle(conversationId, title);
|
||||||
console.log('Konversationstitel aktualisiert:', success ? 'erfolgreich' : 'fehlgeschlagen');
|
console.log('Konversationstitel aktualisiert:', success ? 'erfolgreich' : 'fehlgeschlagen');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userMessageId,
|
userMessageId,
|
||||||
assistantMessageId,
|
assistantMessageId,
|
||||||
assistantResponse: chatResponse,
|
assistantResponse: chatResponse,
|
||||||
title,
|
title,
|
||||||
documentContent
|
documentContent,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Senden der Nachricht:', error);
|
console.error('Fehler beim Senden der Nachricht:', error);
|
||||||
|
|
||||||
// Detaillierte Fehlerinformationen ausgeben
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Fehlerdetails:', {
|
console.error('Fehlerdetails:', {
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userMessageId: null,
|
userMessageId: null,
|
||||||
assistantMessageId: null,
|
assistantMessageId: null,
|
||||||
assistantResponse: `Es ist ein Fehler aufgetreten: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}. Bitte versuche es später erneut.`
|
assistantResponse: `Es ist ein Fehler aufgetreten: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}. Bitte versuche es später erneut.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to calculate token cost
|
||||||
|
function calculateTokenCost(
|
||||||
|
promptTokens: number,
|
||||||
|
completionTokens: number,
|
||||||
|
costSettings?: { prompt_per_1k_tokens?: number; completion_per_1k_tokens?: number }
|
||||||
|
): number {
|
||||||
|
const promptCost = costSettings?.prompt_per_1k_tokens || 0.0001;
|
||||||
|
const completionCost = costSettings?.completion_per_1k_tokens || 0.0002;
|
||||||
|
|
||||||
|
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||||
|
return Number(cost.toFixed(6));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt alle aktiven (nicht archivierten) Konversationen eines Benutzers
|
* Lädt alle aktiven (nicht archivierten) Konversationen eines Benutzers
|
||||||
* Optional: Mit spaceId werden nur Konversationen aus diesem Space geladen
|
|
||||||
*/
|
*/
|
||||||
export async function getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
export async function getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||||
try {
|
try {
|
||||||
let query = supabase
|
const conversations = await conversationApi.getConversations(spaceId);
|
||||||
.from('conversations')
|
return conversations.map(toLocalConversation);
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_archived', false);
|
|
||||||
|
|
||||||
// Wenn eine Space-ID angegeben wurde, filtere nach diesem Space
|
|
||||||
if (spaceId) {
|
|
||||||
query = query.eq('space_id', spaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await query.order('updated_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Laden der Konversationen:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Conversation[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Konversationen:', error);
|
console.error('Fehler beim Laden der Konversationen:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -594,19 +505,8 @@ export async function getConversations(userId: string, spaceId?: string): Promis
|
||||||
*/
|
*/
|
||||||
export async function getArchivedConversations(userId: string): Promise<Conversation[]> {
|
export async function getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const conversations = await conversationApi.getArchivedConversations();
|
||||||
.from('conversations')
|
return conversations.map(toLocalConversation);
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_archived', true)
|
|
||||||
.order('updated_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Laden der archivierten Konversationen:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Conversation[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der archivierten Konversationen:', error);
|
console.error('Fehler beim Laden der archivierten Konversationen:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -618,17 +518,7 @@ export async function getArchivedConversations(userId: string): Promise<Conversa
|
||||||
*/
|
*/
|
||||||
export async function archiveConversation(conversationId: string): Promise<boolean> {
|
export async function archiveConversation(conversationId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
return await conversationApi.archiveConversation(conversationId);
|
||||||
.from('conversations')
|
|
||||||
.update({ is_archived: true })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Archivieren der Konversation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Archivieren der Konversation:', error);
|
console.error('Fehler beim Archivieren der Konversation:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -640,17 +530,7 @@ export async function archiveConversation(conversationId: string): Promise<boole
|
||||||
*/
|
*/
|
||||||
export async function unarchiveConversation(conversationId: string): Promise<boolean> {
|
export async function unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
return await conversationApi.unarchiveConversation(conversationId);
|
||||||
.from('conversations')
|
|
||||||
.update({ is_archived: false })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -662,29 +542,7 @@ export async function unarchiveConversation(conversationId: string): Promise<boo
|
||||||
*/
|
*/
|
||||||
export async function deleteConversation(conversationId: string): Promise<boolean> {
|
export async function deleteConversation(conversationId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Lösche zuerst alle zugehörigen Nachrichten
|
return await conversationApi.deleteConversation(conversationId);
|
||||||
const { error: messagesError } = await supabase
|
|
||||||
.from('messages')
|
|
||||||
.delete()
|
|
||||||
.eq('conversation_id', conversationId);
|
|
||||||
|
|
||||||
if (messagesError) {
|
|
||||||
console.error('Fehler beim Löschen der Nachrichten:', messagesError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lösche dann die Konversation selbst
|
|
||||||
const { error: conversationError } = await supabase
|
|
||||||
.from('conversations')
|
|
||||||
.delete()
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (conversationError) {
|
|
||||||
console.error('Fehler beim Löschen der Konversation:', conversationError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Löschen der Konversation:', error);
|
console.error('Fehler beim Löschen der Konversation:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Document-Service für die Verwaltung von Dokumenten im Dokumentmodus
|
* Document Service - CRUD operations via Backend API
|
||||||
*/
|
*/
|
||||||
import { supabase } from '../utils/supabase';
|
import { documentApi, type Document as ApiDocument } from './api';
|
||||||
|
|
||||||
// Typdefinition für ein Dokument
|
// Re-export type with backwards-compatible naming (snake_case for mobile)
|
||||||
export interface Document {
|
export interface Document {
|
||||||
id: string;
|
id: string;
|
||||||
conversation_id: string;
|
conversation_id: string;
|
||||||
|
|
@ -13,37 +13,39 @@ export interface Document {
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to convert API response to local format
|
||||||
|
function toLocalDocument(doc: ApiDocument): Document {
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
conversation_id: doc.conversationId,
|
||||||
|
version: doc.version,
|
||||||
|
content: doc.content,
|
||||||
|
created_at: doc.createdAt,
|
||||||
|
updated_at: doc.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt ein neues Dokument in einer Konversation
|
* Erstellt ein neues Dokument in einer Konversation
|
||||||
* @param conversationId Die ID der Konversation
|
|
||||||
* @param content Der Inhalt des Dokuments
|
|
||||||
* @returns Das erstellte Dokument oder null bei Fehler
|
|
||||||
*/
|
*/
|
||||||
export async function createDocument(
|
export async function createDocument(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
content: string
|
content: string
|
||||||
): Promise<Document | null> {
|
): Promise<Document | null> {
|
||||||
try {
|
try {
|
||||||
console.log(`Erstelle Dokument für Konversation ${conversationId} mit Inhalt: ${content.substring(0, 50)}...`);
|
console.log(
|
||||||
|
`Erstelle Dokument für Konversation ${conversationId} mit Inhalt: ${content.substring(0, 50)}...`
|
||||||
|
);
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const document = await documentApi.createDocument(conversationId, content);
|
||||||
.from('documents')
|
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
version: 1, // Initiale Version ist immer 1
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!document) {
|
||||||
console.error('Fehler beim Erstellen des Dokuments:', error);
|
console.error('Fehler beim Erstellen des Dokuments');
|
||||||
console.error('Vollständiger Fehler:', JSON.stringify(error));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Dokument erfolgreich erstellt:', data);
|
console.log('Dokument erfolgreich erstellt:', document);
|
||||||
return data as Document;
|
return toLocalDocument(document);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Erstellen des Dokuments:', error);
|
console.error('Fehler beim Erstellen des Dokuments:', error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
@ -55,48 +57,20 @@ export async function createDocument(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt eine neue Version eines Dokuments
|
* Erstellt eine neue Version eines Dokuments
|
||||||
* @param conversationId Die ID der Konversation
|
|
||||||
* @param content Der neue Inhalt des Dokuments
|
|
||||||
* @returns Das erstellte Dokument oder null bei Fehler
|
|
||||||
*/
|
*/
|
||||||
export async function createDocumentVersion(
|
export async function createDocumentVersion(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
content: string
|
content: string
|
||||||
): Promise<Document | null> {
|
): Promise<Document | null> {
|
||||||
try {
|
try {
|
||||||
// Hole die aktuelle höchste Version
|
const document = await documentApi.createDocumentVersion(conversationId, content);
|
||||||
const { data: latestVersionData, error: versionError } = await supabase
|
|
||||||
.from('documents')
|
|
||||||
.select('version')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('version', { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (versionError) {
|
if (!document) {
|
||||||
console.error('Fehler beim Laden der letzten Dokumentversion:', versionError);
|
console.error('Fehler beim Erstellen der neuen Dokumentversion');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVersion = (latestVersionData?.version || 0) + 1;
|
return toLocalDocument(document);
|
||||||
|
|
||||||
// Erstelle eine neue Dokumentversion
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('documents')
|
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
version: newVersion,
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Erstellen der neuen Dokumentversion:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Document;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Erstellen der neuen Dokumentversion:', error);
|
console.error('Fehler beim Erstellen der neuen Dokumentversion:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -105,32 +79,20 @@ export async function createDocumentVersion(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holt die aktuellste Version eines Dokuments für eine Konversation
|
* Holt die aktuellste Version eines Dokuments für eine Konversation
|
||||||
* @param conversationId Die ID der Konversation
|
|
||||||
* @returns Das aktuellste Dokument oder null, wenn nicht gefunden
|
|
||||||
*/
|
*/
|
||||||
export async function getLatestDocument(conversationId: string): Promise<Document | null> {
|
export async function getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||||
try {
|
try {
|
||||||
console.log(`Lade neuestes Dokument für Konversation ${conversationId}`);
|
console.log(`Lade neuestes Dokument für Konversation ${conversationId}`);
|
||||||
|
|
||||||
// Einfache Abfrage ohne Cache-Busting
|
const document = await documentApi.getLatestDocument(conversationId);
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('documents')
|
|
||||||
.select('*')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('version', { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (!document) {
|
||||||
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
console.log('Kein Dokument gefunden');
|
||||||
if (error.code === 'PGRST116') {
|
|
||||||
console.log('Kein Dokument gefunden (PGRST116)');
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Neuestes Dokument gefunden: Version ${data.version}, ID ${data.id}`);
|
console.log(`Neuestes Dokument gefunden: Version ${document.version}, ID ${document.id}`);
|
||||||
return data as Document;
|
return toLocalDocument(document);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -139,35 +101,22 @@ export async function getLatestDocument(conversationId: string): Promise<Documen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt alle Versionen eines Dokuments für eine Konversation
|
* Lädt alle Versionen eines Dokuments für eine Konversation
|
||||||
* @param conversationId Die ID der Konversation
|
|
||||||
* @returns Eine Liste aller Dokumentversionen
|
|
||||||
*/
|
*/
|
||||||
export async function getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
export async function getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||||
try {
|
try {
|
||||||
console.log(`Lade alle Dokumentversionen für Konversation ${conversationId}`);
|
console.log(`Lade alle Dokumentversionen für Konversation ${conversationId}`);
|
||||||
|
|
||||||
// Einfache Abfrage ohne Cache-Busting (das verursacht Probleme)
|
const documents = await documentApi.getAllDocumentVersions(conversationId);
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('documents')
|
|
||||||
.select('*')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('version', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
console.log(`${documents.length} Dokumentversionen geladen`);
|
||||||
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`${data?.length || 0} Dokumentversionen geladen`);
|
if (documents.length > 0) {
|
||||||
|
console.log(`Erstes Dokument: ID=${documents[0].id}, Version=${documents[0].version}`);
|
||||||
// Detaillierte Infos zur Fehlersuche
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
console.log(`Erstes Dokument: ID=${data[0].id}, Version=${data[0].version}`);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Keine Dokumente gefunden');
|
console.log('Keine Dokumente gefunden');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as Document[];
|
return documents.map(toLocalDocument);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -176,22 +125,10 @@ export async function getAllDocumentVersions(conversationId: string): Promise<Do
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft, ob für eine Konversation ein Dokument existiert
|
* Prüft, ob für eine Konversation ein Dokument existiert
|
||||||
* @param conversationId Die ID der Konversation
|
|
||||||
* @returns true, wenn ein Dokument existiert, sonst false
|
|
||||||
*/
|
*/
|
||||||
export async function hasDocument(conversationId: string): Promise<boolean> {
|
export async function hasDocument(conversationId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { count, error } = await supabase
|
return await documentApi.hasDocument(conversationId);
|
||||||
.from('documents')
|
|
||||||
.select('id', { count: 'exact', head: true })
|
|
||||||
.eq('conversation_id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Prüfen auf Dokument:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (count || 0) > 0;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Prüfen auf Dokument:', error);
|
console.error('Fehler beim Prüfen auf Dokument:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -200,47 +137,20 @@ export async function hasDocument(conversationId: string): Promise<boolean> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löscht eine spezifische Dokumentversion
|
* Löscht eine spezifische Dokumentversion
|
||||||
* @param documentId Die ID des zu löschenden Dokuments
|
|
||||||
* @returns true, wenn erfolgreich gelöscht, sonst false
|
|
||||||
*/
|
*/
|
||||||
export async function deleteDocumentVersion(documentId: string): Promise<boolean> {
|
export async function deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
console.log(`=== LÖSCH-OPERATION GESTARTET FÜR DOKUMENT ID ${documentId} ===`);
|
console.log(`=== LÖSCH-OPERATION GESTARTET FÜR DOKUMENT ID ${documentId} ===`);
|
||||||
|
|
||||||
// Prüfe zuerst, ob das Dokument existiert
|
const success = await documentApi.deleteDocumentVersion(documentId);
|
||||||
const { data: existingData, error: checkError } = await supabase
|
|
||||||
.from('documents')
|
|
||||||
.select('id')
|
|
||||||
.eq('id', documentId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (checkError) {
|
|
||||||
console.error('Fehler beim Prüfen, ob das Dokument existiert:', checkError);
|
|
||||||
console.error('Details:', JSON.stringify(checkError));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingData) {
|
|
||||||
console.error('Dokument nicht gefunden:', documentId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Dokument gefunden, führe Löschung durch...');
|
|
||||||
|
|
||||||
// Führe die eigentliche Löschung durch
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('documents')
|
|
||||||
.delete()
|
|
||||||
.eq('id', documentId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Löschen der Dokumentversion:', error);
|
|
||||||
console.error('Vollständige Fehlermeldung:', JSON.stringify(error));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (success) {
|
||||||
console.log(`=== DOKUMENT ${documentId} ERFOLGREICH GELÖSCHT ===`);
|
console.log(`=== DOKUMENT ${documentId} ERFOLGREICH GELÖSCHT ===`);
|
||||||
return true;
|
} else {
|
||||||
|
console.error('Fehler beim Löschen der Dokumentversion');
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unerwarteter Fehler beim Löschen der Dokumentversion:', error);
|
console.error('Unerwarteter Fehler beim Löschen der Dokumentversion:', error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
// Chat Service - AI API Aufrufe werden über das Backend gehandhabt
|
/**
|
||||||
|
* Chat Service - AI API calls via Backend
|
||||||
|
* This service wraps the backend API for AI completions
|
||||||
|
*/
|
||||||
import { availableModels } from '../config/azure';
|
import { availableModels } from '../config/azure';
|
||||||
import { sendChatRequest as sendChatRequestApi } from '../utils/api';
|
import { chatApi, modelApi, usageApi, type ChatMessage, type TokenUsage } from './api';
|
||||||
import { supabase } from '../utils/supabase';
|
|
||||||
|
|
||||||
// Typdefinition für eine Nachricht
|
// Re-export types for backward compatibility
|
||||||
export type ChatMessage = {
|
export type { ChatMessage };
|
||||||
role: 'system' | 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Typdefinition für die Chat-Antwort vom o3-mini-Modell
|
// Re-export TokenUsage
|
||||||
|
export type { TokenUsage };
|
||||||
|
|
||||||
|
// Chat response type (kept for compatibility)
|
||||||
export type ChatResponse = {
|
export type ChatResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
choices: {
|
choices: {
|
||||||
// Für o3-mini-Modell
|
|
||||||
content_filter_results?: any;
|
content_filter_results?: any;
|
||||||
finish_reason: string;
|
finish_reason: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -38,34 +39,20 @@ export type ChatResponse = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Token-Nutzungsinformationen
|
// Return type for chat request
|
||||||
export type TokenUsage = {
|
|
||||||
prompt_tokens: number;
|
|
||||||
completion_tokens: number;
|
|
||||||
total_tokens: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rückgabetyp für die Chat-Anfrage
|
|
||||||
export type ChatRequestResult = {
|
export type ChatRequestResult = {
|
||||||
content: string;
|
content: string;
|
||||||
usage: TokenUsage;
|
usage: TokenUsage;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Backend-URL für sichere API-Aufrufe
|
// Logging configuration
|
||||||
const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001';
|
|
||||||
|
|
||||||
// Logging der Konfiguration
|
|
||||||
console.log('Chat Service Konfiguration:', {
|
console.log('Chat Service Konfiguration:', {
|
||||||
backendUrl: BACKEND_URL,
|
backendUrl: process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001',
|
||||||
availableModels: availableModels.length,
|
availableModels: availableModels.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Berechnet die geschätzten Kosten einer LLM-Anfrage
|
* Calculates estimated cost for an LLM request
|
||||||
* @param promptTokens Anzahl der Eingabe-Tokens
|
|
||||||
* @param completionTokens Anzahl der Ausgabe-Tokens
|
|
||||||
* @param modelId ID des verwendeten Modells
|
|
||||||
* @returns Geschätzte Kosten in der kleinsten Währungseinheit (z.B. Cent)
|
|
||||||
*/
|
*/
|
||||||
export async function calculateTokenCost(
|
export async function calculateTokenCost(
|
||||||
promptTokens: number,
|
promptTokens: number,
|
||||||
|
|
@ -73,45 +60,31 @@ export async function calculateTokenCost(
|
||||||
modelId: string
|
modelId: string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
// Hole die Kosteninformationen aus dem Modell
|
// Get cost settings from model
|
||||||
const { data: modelData, error } = await supabase
|
const modelData = await modelApi.getModel(modelId);
|
||||||
.from('models')
|
|
||||||
.select('cost_settings')
|
|
||||||
.eq('id', modelId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error || !modelData || !modelData.cost_settings) {
|
if (!modelData || !modelData.costSettings) {
|
||||||
console.warn('Fehler beim Laden der Kosteninformationen, verwende Standardwerte:', error);
|
console.warn('Fehler beim Laden der Kosteninformationen, verwende Standardwerte');
|
||||||
// Standardwerte verwenden
|
const promptCost = 0.0001;
|
||||||
const promptCost = 0.0001; // pro 1K Tokens
|
const completionCost = 0.0002;
|
||||||
const completionCost = 0.0002; // pro 1K Tokens
|
|
||||||
|
|
||||||
// Berechne die Kosten
|
|
||||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||||
return Number(cost.toFixed(6));
|
return Number(cost.toFixed(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrahiere die Kostensätze
|
const promptCost = modelData.costSettings.prompt_per_1k_tokens || 0.0001;
|
||||||
const promptCost = parseFloat(modelData.cost_settings.prompt_per_1k_tokens) || 0.0001;
|
const completionCost = modelData.costSettings.completion_per_1k_tokens || 0.0002;
|
||||||
const completionCost = parseFloat(modelData.cost_settings.completion_per_1k_tokens) || 0.0002;
|
|
||||||
|
|
||||||
// Berechne die Kosten
|
|
||||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||||
return Number(cost.toFixed(6));
|
return Number(cost.toFixed(6));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei der Kostenberechnung:', error);
|
console.error('Fehler bei der Kostenberechnung:', error);
|
||||||
// Fallback: vereinfachte Berechnung
|
|
||||||
return Number(((promptTokens * 0.0001 + completionTokens * 0.0002) / 1000).toFixed(6));
|
return Number(((promptTokens * 0.0001 + completionTokens * 0.0002) / 1000).toFixed(6));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speichert die Token-Nutzung in der Datenbank
|
* Logs token usage to the database
|
||||||
* @param usage Token-Nutzungsinformationen
|
|
||||||
* @param conversationId ID der Konversation
|
|
||||||
* @param messageId ID der Nachricht
|
|
||||||
* @param userId ID des Benutzers
|
|
||||||
* @param modelId ID des verwendeten Modells
|
|
||||||
*/
|
*/
|
||||||
export async function logTokenUsage(
|
export async function logTokenUsage(
|
||||||
usage: TokenUsage,
|
usage: TokenUsage,
|
||||||
|
|
@ -121,36 +94,31 @@ export async function logTokenUsage(
|
||||||
modelId: string
|
modelId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Berechne die geschätzten Kosten
|
|
||||||
const estimatedCost = await calculateTokenCost(
|
const estimatedCost = await calculateTokenCost(
|
||||||
usage.prompt_tokens,
|
usage.prompt_tokens,
|
||||||
usage.completion_tokens,
|
usage.completion_tokens,
|
||||||
modelId
|
modelId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Speichere die Nutzungsinformationen
|
const success = await usageApi.logTokenUsage({
|
||||||
const { error } = await supabase
|
conversationId,
|
||||||
.from('usage_logs')
|
messageId,
|
||||||
.insert({
|
modelId,
|
||||||
conversation_id: conversationId,
|
promptTokens: usage.prompt_tokens,
|
||||||
message_id: messageId,
|
completionTokens: usage.completion_tokens,
|
||||||
user_id: userId,
|
totalTokens: usage.total_tokens,
|
||||||
model_id: modelId,
|
estimatedCost,
|
||||||
prompt_tokens: usage.prompt_tokens,
|
|
||||||
completion_tokens: usage.completion_tokens,
|
|
||||||
total_tokens: usage.total_tokens,
|
|
||||||
estimated_cost: estimatedCost
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (success) {
|
||||||
console.error('Fehler beim Speichern der Token-Nutzung:', error);
|
|
||||||
} else {
|
|
||||||
console.log('Token-Nutzung erfolgreich gespeichert:', {
|
console.log('Token-Nutzung erfolgreich gespeichert:', {
|
||||||
conversationId,
|
conversationId,
|
||||||
messageId,
|
messageId,
|
||||||
totalTokens: usage.total_tokens,
|
totalTokens: usage.total_tokens,
|
||||||
estimatedCost
|
estimatedCost,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Fehler beim Speichern der Token-Nutzung');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
||||||
|
|
@ -158,12 +126,7 @@ export async function logTokenUsage(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet eine Chat-Anfrage über das Backend
|
* Sends a chat request via the backend
|
||||||
* Das Backend handhabt die Azure OpenAI API Aufrufe sicher
|
|
||||||
* @param messages Array von Nachrichten im Chat
|
|
||||||
* @param temperature Kreativität der Antwort (0.0 - 1.0)
|
|
||||||
* @param maxTokens Maximale Anzahl der Tokens in der Antwort
|
|
||||||
* @returns Die Antwort des LLM-Modells und Tokeninformationen
|
|
||||||
*/
|
*/
|
||||||
export async function sendChatRequest(
|
export async function sendChatRequest(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
|
|
@ -172,57 +135,66 @@ export async function sendChatRequest(
|
||||||
): Promise<string | ChatRequestResult> {
|
): Promise<string | ChatRequestResult> {
|
||||||
console.log('sendChatRequest gestartet mit:', {
|
console.log('sendChatRequest gestartet mit:', {
|
||||||
messagesCount: messages.length,
|
messagesCount: messages.length,
|
||||||
maxTokens
|
maxTokens,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Hole aktuelle Modellparameter aus der Konversation (für Modellwechsel)
|
// Find model deployment from system message
|
||||||
let deployment = '';
|
let modelId = '550e8400-e29b-41d4-a716-446655440000'; // Default to GPT-O3-Mini
|
||||||
|
|
||||||
// System-Nachricht mit Modell-Präfix suchen
|
const systemMessage = messages.find(
|
||||||
const systemMessage = messages.find(msg => msg.role === 'system' && msg.content.startsWith('MODEL:'));
|
(msg) => msg.role === 'system' && msg.content.startsWith('MODEL:')
|
||||||
|
);
|
||||||
if (systemMessage) {
|
if (systemMessage) {
|
||||||
deployment = systemMessage.content.split(':')[1].trim();
|
const deployment = systemMessage.content.split(':')[1].trim();
|
||||||
console.log('Modell in system Nachricht erkannt:', deployment);
|
console.log('Modell in system Nachricht erkannt:', deployment);
|
||||||
|
|
||||||
|
// Map deployment to model ID
|
||||||
|
const deploymentToModelId: Record<string, string> = {
|
||||||
|
'gpt-o3-mini-se': '550e8400-e29b-41d4-a716-446655440000',
|
||||||
|
'gpt-4o-mini-se': '550e8400-e29b-41d4-a716-446655440004',
|
||||||
|
'gpt-4o-se': '550e8400-e29b-41d4-a716-446655440005',
|
||||||
|
};
|
||||||
|
|
||||||
|
modelId = deploymentToModelId[deployment] || modelId;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Keine System-Nachricht mit MODEL-Präfix gefunden!');
|
console.warn('Keine System-Nachricht mit MODEL-Präfix gefunden!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Falls kein Modell angegeben wurde, setze auf Fallback gpt-o3-mini-se
|
console.log('Verwende Model ID:', modelId);
|
||||||
const deploymentToUse = deployment || 'gpt-o3-mini-se';
|
|
||||||
console.log('Verwende Deployment:', deploymentToUse);
|
|
||||||
|
|
||||||
// Konfiguration für den API-Wrapper (Backend benötigt nur deployment)
|
// Filter out MODEL: system messages before sending to API
|
||||||
const config = {
|
const filteredMessages = messages.filter(
|
||||||
deployment: deploymentToUse
|
(msg) => !(msg.role === 'system' && msg.content.startsWith('MODEL:'))
|
||||||
};
|
);
|
||||||
|
|
||||||
// Verwende den zentralen API-Wrapper - dieser ruft das Backend auf
|
// Send request to backend
|
||||||
const result = await sendChatRequestApi(messages, temperature, maxTokens, config);
|
const result = await chatApi.createCompletion({
|
||||||
|
messages: filteredMessages,
|
||||||
|
modelId,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
});
|
||||||
|
|
||||||
// Wenn es ein einfacher String ist (Fehlerfall), diesen zurückgeben
|
if (!result) {
|
||||||
if (typeof result === 'string') {
|
return 'Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft.';
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ansonsten die vollständige Antwort mit Token-Nutzung zurückgeben
|
|
||||||
return {
|
return {
|
||||||
content: result.content,
|
content: result.content,
|
||||||
usage: result.usage
|
usage: result.usage,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei der Chat-Anfrage:', error);
|
console.error('Fehler bei der Chat-Anfrage:', error);
|
||||||
|
|
||||||
// Versuche, mehr Informationen über den Fehler zu erhalten
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Fehlerdetails:', {
|
console.error('Fehlerdetails:', {
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gib eine benutzerfreundliche Fehlermeldung zurück, anstatt den Fehler zu werfen
|
|
||||||
return `Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft. Fehlerdetails: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`;
|
return `Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft. Fehlerdetails: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { supabase } from '../utils/supabase';
|
/**
|
||||||
|
* Space Service - CRUD operations via Backend API
|
||||||
|
*/
|
||||||
|
import { spaceApi, type Space as ApiSpace, type SpaceMember as ApiSpaceMember } from './api';
|
||||||
|
|
||||||
// Type definitions for spaces and members
|
// Re-export types with backwards-compatible naming (snake_case for mobile)
|
||||||
export type Space = {
|
export type Space = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -24,190 +27,140 @@ export type SpaceMember = {
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get all spaces for a user (both owned and member of)
|
// Helper to convert API response to local format
|
||||||
|
function toLocalSpace(space: ApiSpace): Space {
|
||||||
|
return {
|
||||||
|
id: space.id,
|
||||||
|
name: space.name,
|
||||||
|
description: space.description,
|
||||||
|
owner_id: space.ownerId,
|
||||||
|
created_at: space.createdAt,
|
||||||
|
updated_at: space.updatedAt,
|
||||||
|
is_archived: space.isArchived,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalSpaceMember(member: ApiSpaceMember): SpaceMember {
|
||||||
|
return {
|
||||||
|
id: member.id,
|
||||||
|
space_id: member.spaceId,
|
||||||
|
user_id: member.userId,
|
||||||
|
role: member.role,
|
||||||
|
invitation_status: member.invitationStatus,
|
||||||
|
invited_by: member.invitedBy,
|
||||||
|
invited_at: member.invitedAt,
|
||||||
|
joined_at: member.joinedAt,
|
||||||
|
created_at: member.createdAt,
|
||||||
|
updated_at: member.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all spaces for a user (both owned and member of)
|
||||||
|
*/
|
||||||
export async function getUserSpaces(userId: string): Promise<Space[]> {
|
export async function getUserSpaces(userId: string): Promise<Space[]> {
|
||||||
try {
|
try {
|
||||||
const { data: memberData, error: memberError } = await supabase
|
const spaces = await spaceApi.getUserSpaces();
|
||||||
.from('space_members')
|
return spaces.map(toLocalSpace);
|
||||||
.select(`
|
|
||||||
space_id,
|
|
||||||
role,
|
|
||||||
invitation_status
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('invitation_status', 'accepted');
|
|
||||||
|
|
||||||
if (memberError) {
|
|
||||||
console.error('Error fetching user space memberships:', memberError);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!memberData || memberData.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get space IDs the user is a member of
|
|
||||||
const spaceIds = memberData.map(m => m.space_id);
|
|
||||||
|
|
||||||
// Fetch the actual space data
|
|
||||||
const { data: spaces, error: spacesError } = await supabase
|
|
||||||
.from('spaces')
|
|
||||||
.select('*')
|
|
||||||
.in('id', spaceIds)
|
|
||||||
.eq('is_archived', false)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (spacesError) {
|
|
||||||
console.error('Error fetching spaces:', spacesError);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return spaces as Space[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getUserSpaces:', error);
|
console.error('Error in getUserSpaces:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get spaces the user owns
|
/**
|
||||||
|
* Get spaces the user owns
|
||||||
|
*/
|
||||||
export async function getOwnedSpaces(userId: string): Promise<Space[]> {
|
export async function getOwnedSpaces(userId: string): Promise<Space[]> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const spaces = await spaceApi.getOwnedSpaces();
|
||||||
.from('spaces')
|
return spaces.map(toLocalSpace);
|
||||||
.select('*')
|
|
||||||
.eq('owner_id', userId)
|
|
||||||
.eq('is_archived', false)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching owned spaces:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Space[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getOwnedSpaces:', error);
|
console.error('Error in getOwnedSpaces:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single space by ID
|
/**
|
||||||
|
* Get a single space by ID
|
||||||
|
*/
|
||||||
export async function getSpace(spaceId: string): Promise<Space | null> {
|
export async function getSpace(spaceId: string): Promise<Space | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const space = await spaceApi.getSpace(spaceId);
|
||||||
.from('spaces')
|
if (!space) {
|
||||||
.select('*')
|
|
||||||
.eq('id', spaceId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching space:', error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return toLocalSpace(space);
|
||||||
return data as Space;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getSpace:', error);
|
console.error('Error in getSpace:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new space
|
/**
|
||||||
|
* Create a new space
|
||||||
|
*/
|
||||||
export async function createSpace(
|
export async function createSpace(
|
||||||
userId: string,
|
userId: string,
|
||||||
name: string,
|
name: string,
|
||||||
description?: string
|
description?: string
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const space = await spaceApi.createSpace(name, description);
|
||||||
.from('spaces')
|
return space?.id || null;
|
||||||
.insert({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
owner_id: userId
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error creating space:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.id;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in createSpace:', error);
|
console.error('Error in createSpace:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update a space
|
/**
|
||||||
|
* Update a space
|
||||||
|
*/
|
||||||
export async function updateSpace(
|
export async function updateSpace(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
updates: { name?: string; description?: string; is_archived?: boolean }
|
updates: { name?: string; description?: string; is_archived?: boolean }
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
return await spaceApi.updateSpace(spaceId, {
|
||||||
.from('spaces')
|
name: updates.name,
|
||||||
.update(updates)
|
description: updates.description,
|
||||||
.eq('id', spaceId);
|
isArchived: updates.is_archived,
|
||||||
|
});
|
||||||
if (error) {
|
|
||||||
console.error('Error updating space:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in updateSpace:', error);
|
console.error('Error in updateSpace:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a space
|
/**
|
||||||
|
* Delete a space
|
||||||
|
*/
|
||||||
export async function deleteSpace(spaceId: string): Promise<boolean> {
|
export async function deleteSpace(spaceId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Delete the space (members will be cascade deleted due to foreign key constraint)
|
return await spaceApi.deleteSpace(spaceId);
|
||||||
const { error } = await supabase
|
|
||||||
.from('spaces')
|
|
||||||
.delete()
|
|
||||||
.eq('id', spaceId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error deleting space:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in deleteSpace:', error);
|
console.error('Error in deleteSpace:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get members of a space
|
/**
|
||||||
|
* Get members of a space
|
||||||
|
*/
|
||||||
export async function getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
export async function getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const members = await spaceApi.getSpaceMembers(spaceId);
|
||||||
.from('space_members')
|
return members.map(toLocalSpaceMember);
|
||||||
.select('*')
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.order('role', { ascending: true })
|
|
||||||
.order('joined_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching space members:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as SpaceMember[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getSpaceMembers:', error);
|
console.error('Error in getSpaceMembers:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a member to a space
|
/**
|
||||||
|
* Add a member to a space (invite)
|
||||||
|
*/
|
||||||
export async function inviteUserToSpace(
|
export async function inviteUserToSpace(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|
@ -215,228 +168,83 @@ export async function inviteUserToSpace(
|
||||||
role: 'admin' | 'member' | 'viewer' = 'member'
|
role: 'admin' | 'member' | 'viewer' = 'member'
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Check if user is already a member
|
return await spaceApi.inviteUser(spaceId, userId, role);
|
||||||
const { data: existingMember, error: checkError } = await supabase
|
|
||||||
.from('space_members')
|
|
||||||
.select('id, invitation_status')
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (checkError) {
|
|
||||||
console.error('Error checking existing membership:', checkError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If already a member with accepted status, just return true
|
|
||||||
if (existingMember && existingMember.invitation_status === 'accepted') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a pending or declined invitation, update it
|
|
||||||
if (existingMember) {
|
|
||||||
const { error: updateError } = await supabase
|
|
||||||
.from('space_members')
|
|
||||||
.update({
|
|
||||||
role,
|
|
||||||
invitation_status: 'pending',
|
|
||||||
invited_by: invitedByUserId,
|
|
||||||
invited_at: new Date().toISOString()
|
|
||||||
})
|
|
||||||
.eq('id', existingMember.id);
|
|
||||||
|
|
||||||
if (updateError) {
|
|
||||||
console.error('Error updating invitation:', updateError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, create a new invitation
|
|
||||||
const { error: insertError } = await supabase
|
|
||||||
.from('space_members')
|
|
||||||
.insert({
|
|
||||||
space_id: spaceId,
|
|
||||||
user_id: userId,
|
|
||||||
role,
|
|
||||||
invited_by: invitedByUserId,
|
|
||||||
invitation_status: 'pending'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (insertError) {
|
|
||||||
console.error('Error inviting user to space:', insertError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in inviteUserToSpace:', error);
|
console.error('Error in inviteUserToSpace:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept or decline a space invitation
|
/**
|
||||||
|
* Accept or decline a space invitation
|
||||||
|
*/
|
||||||
export async function respondToInvitation(
|
export async function respondToInvitation(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
status: 'accepted' | 'declined'
|
status: 'accepted' | 'declined'
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const updates: any = {
|
return await spaceApi.respondToInvitation(spaceId, status);
|
||||||
invitation_status: status
|
|
||||||
};
|
|
||||||
|
|
||||||
// If accepting, set the joined_at timestamp
|
|
||||||
if (status === 'accepted') {
|
|
||||||
updates.joined_at = new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('space_members')
|
|
||||||
.update(updates)
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error responding to invitation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in respondToInvitation:', error);
|
console.error('Error in respondToInvitation:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a member from a space
|
/**
|
||||||
|
* Remove a member from a space
|
||||||
|
*/
|
||||||
export async function removeMember(spaceId: string, userId: string): Promise<boolean> {
|
export async function removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
return await spaceApi.removeMember(spaceId, userId);
|
||||||
.from('space_members')
|
|
||||||
.delete()
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error removing member:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in removeMember:', error);
|
console.error('Error in removeMember:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change a member's role
|
/**
|
||||||
|
* Change a member's role
|
||||||
|
*/
|
||||||
export async function changeMemberRole(
|
export async function changeMemberRole(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
newRole: 'admin' | 'member' | 'viewer'
|
newRole: 'admin' | 'member' | 'viewer'
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
return await spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||||
.from('space_members')
|
|
||||||
.update({ role: newRole })
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error changing member role:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in changeMemberRole:', error);
|
console.error('Error in changeMemberRole:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's role in a space
|
/**
|
||||||
|
* Get user's role in a space
|
||||||
|
*/
|
||||||
export async function getUserRoleInSpace(
|
export async function getUserRoleInSpace(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||||
try {
|
try {
|
||||||
// First check if they're the owner
|
return await spaceApi.getUserRoleInSpace(spaceId);
|
||||||
const { data: space, error: spaceError } = await supabase
|
|
||||||
.from('spaces')
|
|
||||||
.select('owner_id')
|
|
||||||
.eq('id', spaceId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (spaceError) {
|
|
||||||
console.error('Error checking space ownership:', spaceError);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (space.owner_id === userId) {
|
|
||||||
return 'owner';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not owner, check membership
|
|
||||||
const { data: member, error: memberError } = await supabase
|
|
||||||
.from('space_members')
|
|
||||||
.select('role, invitation_status')
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (memberError) {
|
|
||||||
// This could mean they're not a member, which is fine
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only return role if invitation is accepted
|
|
||||||
if (member && member.invitation_status === 'accepted') {
|
|
||||||
return member.role as 'admin' | 'member' | 'viewer';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getUserRoleInSpace:', error);
|
console.error('Error in getUserRoleInSpace:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get pending space invitations for a user
|
/**
|
||||||
export async function getPendingInvitations(userId: string): Promise<Array<{
|
* Get pending space invitations for a user
|
||||||
invitation: SpaceMember;
|
*/
|
||||||
space: Space;
|
export async function getPendingInvitations(
|
||||||
}>> {
|
userId: string
|
||||||
|
): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const invitations = await spaceApi.getPendingInvitations();
|
||||||
.from('space_members')
|
return invitations.map((inv) => ({
|
||||||
.select(`
|
invitation: toLocalSpaceMember(inv.invitation),
|
||||||
*,
|
space: toLocalSpace(inv.space),
|
||||||
space:space_id (*)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('invitation_status', 'pending');
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching pending invitations:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.map(item => ({
|
|
||||||
invitation: {
|
|
||||||
id: item.id,
|
|
||||||
space_id: item.space_id,
|
|
||||||
user_id: item.user_id,
|
|
||||||
role: item.role,
|
|
||||||
invitation_status: item.invitation_status,
|
|
||||||
invited_by: item.invited_by,
|
|
||||||
invited_at: item.invited_at,
|
|
||||||
joined_at: item.joined_at,
|
|
||||||
created_at: item.created_at,
|
|
||||||
updated_at: item.updated_at
|
|
||||||
},
|
|
||||||
space: item.space
|
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getPendingInvitations:', error);
|
console.error('Error in getPendingInvitations:', error);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Template-Service für Chat-Vorlagen
|
* Template Service - CRUD operations via Backend API
|
||||||
*/
|
*/
|
||||||
import { supabase } from '../utils/supabase';
|
import { templateApi, type Template as ApiTemplate } from './api';
|
||||||
|
|
||||||
// Typdefinition für eine Vorlage
|
// Re-export type with backwards-compatible naming (snake_case for mobile)
|
||||||
export interface Template {
|
export interface Template {
|
||||||
id: string;
|
id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
|
@ -19,25 +19,31 @@ export interface Template {
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to convert API response to local format
|
||||||
|
function toLocalTemplate(template: ApiTemplate): Template {
|
||||||
|
return {
|
||||||
|
id: template.id,
|
||||||
|
user_id: template.userId,
|
||||||
|
name: template.name,
|
||||||
|
description: template.description || null,
|
||||||
|
system_prompt: template.systemPrompt,
|
||||||
|
initial_question: template.initialQuestion || null,
|
||||||
|
model_id: template.modelId || null,
|
||||||
|
color: template.color,
|
||||||
|
is_default: template.isDefault,
|
||||||
|
document_mode: template.documentMode,
|
||||||
|
created_at: template.createdAt,
|
||||||
|
updated_at: template.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt alle Vorlagen eines Benutzers
|
* Lädt alle Vorlagen eines Benutzers
|
||||||
* @param userId Die ID des Benutzers
|
|
||||||
* @returns Liste der Vorlagen
|
|
||||||
*/
|
*/
|
||||||
export async function getTemplates(userId: string): Promise<Template[]> {
|
export async function getTemplates(userId: string): Promise<Template[]> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const templates = await templateApi.getTemplates();
|
||||||
.from('templates')
|
return templates.map(toLocalTemplate);
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.order('name');
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Template[];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -46,23 +52,14 @@ export async function getTemplates(userId: string): Promise<Template[]> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt eine bestimmte Vorlage anhand ihrer ID
|
* Lädt eine bestimmte Vorlage anhand ihrer ID
|
||||||
* @param templateId Die ID der Vorlage
|
|
||||||
* @returns Die Vorlage oder null, wenn nicht gefunden
|
|
||||||
*/
|
*/
|
||||||
export async function getTemplateById(templateId: string): Promise<Template | null> {
|
export async function getTemplateById(templateId: string): Promise<Template | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const template = await templateApi.getTemplate(templateId);
|
||||||
.from('templates')
|
if (!template) {
|
||||||
.select('*')
|
|
||||||
.eq('id', templateId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Laden der Vorlage:', error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return toLocalTemplate(template);
|
||||||
return data as Template;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Vorlage:', error);
|
console.error('Fehler beim Laden der Vorlage:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -71,23 +68,27 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt eine neue Vorlage
|
* Erstellt eine neue Vorlage
|
||||||
* @param template Die zu erstellende Vorlage (ohne ID)
|
|
||||||
* @returns Die erstellte Vorlage mit ID oder null bei Fehler
|
|
||||||
*/
|
*/
|
||||||
export async function createTemplate(template: Omit<Template, 'id' | 'created_at' | 'updated_at'>): Promise<Template | null> {
|
export async function createTemplate(
|
||||||
|
template: Omit<Template, 'id' | 'created_at' | 'updated_at'>
|
||||||
|
): Promise<Template | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const result = await templateApi.createTemplate({
|
||||||
.from('templates')
|
name: template.name,
|
||||||
.insert(template)
|
description: template.description || undefined,
|
||||||
.select()
|
systemPrompt: template.system_prompt,
|
||||||
.single();
|
initialQuestion: template.initial_question || undefined,
|
||||||
|
modelId: template.model_id || undefined,
|
||||||
|
color: template.color,
|
||||||
|
documentMode: template.document_mode,
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (!result) {
|
||||||
console.error('Fehler beim Erstellen der Vorlage:', error);
|
console.error('Fehler beim Erstellen der Vorlage');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as Template;
|
return toLocalTemplate(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Erstellen der Vorlage:', error);
|
console.error('Fehler beim Erstellen der Vorlage:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -96,26 +97,25 @@ export async function createTemplate(template: Omit<Template, 'id' | 'created_at
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aktualisiert eine bestehende Vorlage
|
* Aktualisiert eine bestehende Vorlage
|
||||||
* @param templateId Die ID der zu aktualisierenden Vorlage
|
|
||||||
* @param updates Die zu aktualisierenden Felder
|
|
||||||
* @returns true bei Erfolg, false bei Fehler
|
|
||||||
*/
|
*/
|
||||||
export async function updateTemplate(
|
export async function updateTemplate(
|
||||||
templateId: string,
|
templateId: string,
|
||||||
updates: Partial<Omit<Template, 'id' | 'user_id' | 'created_at' | 'updated_at'>>
|
updates: Partial<Omit<Template, 'id' | 'user_id' | 'created_at' | 'updated_at'>>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
const apiUpdates: Parameters<typeof templateApi.updateTemplate>[1] = {};
|
||||||
.from('templates')
|
|
||||||
.update(updates)
|
|
||||||
.eq('id', templateId);
|
|
||||||
|
|
||||||
if (error) {
|
if (updates.name !== undefined) apiUpdates.name = updates.name;
|
||||||
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
if (updates.description !== undefined)
|
||||||
return false;
|
apiUpdates.description = updates.description || undefined;
|
||||||
}
|
if (updates.system_prompt !== undefined) apiUpdates.systemPrompt = updates.system_prompt;
|
||||||
|
if (updates.initial_question !== undefined)
|
||||||
|
apiUpdates.initialQuestion = updates.initial_question || undefined;
|
||||||
|
if (updates.model_id !== undefined) apiUpdates.modelId = updates.model_id || undefined;
|
||||||
|
if (updates.color !== undefined) apiUpdates.color = updates.color;
|
||||||
|
if (updates.document_mode !== undefined) apiUpdates.documentMode = updates.document_mode;
|
||||||
|
|
||||||
return true;
|
return await templateApi.updateTemplate(templateId, apiUpdates);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -124,22 +124,10 @@ export async function updateTemplate(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löscht eine Vorlage
|
* Löscht eine Vorlage
|
||||||
* @param templateId Die ID der zu löschenden Vorlage
|
|
||||||
* @returns true bei Erfolg, false bei Fehler
|
|
||||||
*/
|
*/
|
||||||
export async function deleteTemplate(templateId: string): Promise<boolean> {
|
export async function deleteTemplate(templateId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
return await templateApi.deleteTemplate(templateId);
|
||||||
.from('templates')
|
|
||||||
.delete()
|
|
||||||
.eq('id', templateId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Löschen der Vorlage:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Löschen der Vorlage:', error);
|
console.error('Fehler beim Löschen der Vorlage:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -148,31 +136,10 @@ export async function deleteTemplate(templateId: string): Promise<boolean> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setzt eine Vorlage als Standard
|
* Setzt eine Vorlage als Standard
|
||||||
* @param templateId Die ID der Vorlage, die als Standard gesetzt werden soll
|
|
||||||
* @param userId Die ID des Benutzers
|
|
||||||
* @returns true bei Erfolg, false bei Fehler
|
|
||||||
*/
|
*/
|
||||||
export async function setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
export async function setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Zuerst alle Vorlagen des Benutzers auf nicht-Standard setzen
|
return await templateApi.setDefaultTemplate(templateId);
|
||||||
await supabase
|
|
||||||
.from('templates')
|
|
||||||
.update({ is_default: false })
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
// Dann die ausgewählte Vorlage als Standard setzen
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('templates')
|
|
||||||
.update({ is_default: true })
|
|
||||||
.eq('id', templateId)
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Setzen der Standard-Vorlage:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Setzen der Standard-Vorlage:', error);
|
console.error('Fehler beim Setzen der Standard-Vorlage:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -181,24 +148,14 @@ export async function setDefaultTemplate(templateId: string, userId: string): Pr
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holt die Standard-Vorlage des Benutzers
|
* Holt die Standard-Vorlage des Benutzers
|
||||||
* @param userId Die ID des Benutzers
|
|
||||||
* @returns Die Standard-Vorlage oder null, wenn keine gefunden wurde
|
|
||||||
*/
|
*/
|
||||||
export async function getDefaultTemplate(userId: string): Promise<Template | null> {
|
export async function getDefaultTemplate(userId: string): Promise<Template | null> {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const template = await templateApi.getDefaultTemplate();
|
||||||
.from('templates')
|
if (!template) {
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_default', true)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return toLocalTemplate(template);
|
||||||
return data as Template;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chat/types": "workspace:*",
|
"@chat/types": "workspace:*",
|
||||||
|
"@manacore/shared-auth": "workspace:*",
|
||||||
"@manacore/shared-auth-ui": "workspace:*",
|
"@manacore/shared-auth-ui": "workspace:*",
|
||||||
"@manacore/shared-branding": "workspace:*",
|
"@manacore/shared-branding": "workspace:*",
|
||||||
"@manacore/shared-i18n": "workspace:*",
|
"@manacore/shared-i18n": "workspace:*",
|
||||||
|
|
|
||||||
7
chat/apps/web/src/app.d.ts
vendored
7
chat/apps/web/src/app.d.ts
vendored
|
|
@ -1,17 +1,14 @@
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
import type { SupabaseClient, Session, User } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
supabase: SupabaseClient;
|
// Auth is now handled client-side via Mana Core Auth
|
||||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>;
|
|
||||||
}
|
}
|
||||||
interface PageData {
|
interface PageData {
|
||||||
session: Session | null;
|
pathname?: string;
|
||||||
user: User | null;
|
|
||||||
}
|
}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Server Hooks for SvelteKit
|
* Server Hooks for SvelteKit
|
||||||
* Handles Supabase session management
|
* Auth is now handled client-side via Mana Core Auth
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { createSupabaseServerClient } from '$lib/services/supabase';
|
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
// Create Supabase client for this request
|
return resolve(event);
|
||||||
event.locals.supabase = createSupabaseServerClient(event.cookies);
|
|
||||||
|
|
||||||
// Get session
|
|
||||||
event.locals.safeGetSession = async () => {
|
|
||||||
const {
|
|
||||||
data: { session },
|
|
||||||
} = await event.locals.supabase.auth.getSession();
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return { session: null, user: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate user (not just reading from cookies)
|
|
||||||
const {
|
|
||||||
data: { user },
|
|
||||||
error,
|
|
||||||
} = await event.locals.supabase.auth.getUser();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return { session: null, user: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { session, user };
|
|
||||||
};
|
|
||||||
|
|
||||||
return resolve(event, {
|
|
||||||
filterSerializedResponseHeaders(name) {
|
|
||||||
return name === 'content-range' || name === 'x-supabase-api-version';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,682 @@
|
||||||
/**
|
/**
|
||||||
* Backend API Client for Chat
|
* API Client for Chat Backend
|
||||||
|
*
|
||||||
|
* This replaces direct Supabase calls with backend API calls.
|
||||||
|
* All database operations now go through the NestJS backend.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
const BACKEND_URL = env.PUBLIC_BACKEND_URL || 'http://localhost:3001';
|
const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3002';
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
type FetchOptions = {
|
||||||
data?: T;
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
||||||
error?: string;
|
body?: unknown;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchApi<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: FetchOptions = {},
|
||||||
|
): Promise<{ data: T | null; error: Error | null }> {
|
||||||
|
const { method = 'GET', body, token } = options;
|
||||||
|
|
||||||
|
// Get token from localStorage if not provided
|
||||||
|
let authToken = token;
|
||||||
|
if (!authToken && browser) {
|
||||||
|
authToken = localStorage.getItem('mana_token') || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authToken) {
|
||||||
|
return { data: null, error: new Error('No authentication token') };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequest<T>(
|
|
||||||
endpoint: string,
|
|
||||||
options: RequestInit = {}
|
|
||||||
): Promise<ApiResponse<T>> {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BACKEND_URL}${endpoint}`, {
|
const response = await fetch(`${API_BASE}/api${endpoint}`, {
|
||||||
...options,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...options.headers,
|
Authorization: `Bearer ${authToken}`,
|
||||||
},
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorData = await response.json().catch(() => ({}));
|
||||||
return { error: errorText || `HTTP ${response.status}` };
|
return {
|
||||||
|
data: null,
|
||||||
|
error: new Error(errorData.message || `API error: ${response.status}`),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return { data };
|
return { data, error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error : new Error('Unknown error'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
// ============ Conversation API ============
|
||||||
get: <T>(endpoint: string) => apiRequest<T>(endpoint, { method: 'GET' }),
|
|
||||||
post: <T>(endpoint: string, body: unknown) =>
|
export type Conversation = {
|
||||||
apiRequest<T>(endpoint, {
|
id: string;
|
||||||
method: 'POST',
|
userId: string;
|
||||||
body: JSON.stringify(body),
|
modelId: string;
|
||||||
}),
|
templateId?: string;
|
||||||
put: <T>(endpoint: string, body: unknown) =>
|
spaceId?: string;
|
||||||
apiRequest<T>(endpoint, {
|
title?: string;
|
||||||
method: 'PUT',
|
conversationMode: 'free' | 'guided' | 'template';
|
||||||
body: JSON.stringify(body),
|
documentMode: boolean;
|
||||||
}),
|
isArchived: boolean;
|
||||||
delete: <T>(endpoint: string) => apiRequest<T>(endpoint, { method: 'DELETE' }),
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
sender: 'user' | 'assistant' | 'system';
|
||||||
|
messageText: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const conversationApi = {
|
||||||
|
async getConversations(spaceId?: string): Promise<Conversation[]> {
|
||||||
|
const query = spaceId ? `?spaceId=${spaceId}` : '';
|
||||||
|
const { data, error } = await fetchApi<Conversation[]>(`/conversations${query}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading conversations:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getArchivedConversations(): Promise<Conversation[]> {
|
||||||
|
const { data, error } = await fetchApi<Conversation[]>('/conversations/archived');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading archived conversations:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getConversation(id: string): Promise<Conversation | null> {
|
||||||
|
const { data, error } = await fetchApi<Conversation>(`/conversations/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading conversation:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getMessages(conversationId: string): Promise<Message[]> {
|
||||||
|
const { data, error } = await fetchApi<Message[]>(
|
||||||
|
`/conversations/${conversationId}/messages`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading messages:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async createConversation(options: {
|
||||||
|
modelId: string;
|
||||||
|
title?: string;
|
||||||
|
templateId?: string;
|
||||||
|
conversationMode?: 'free' | 'guided' | 'template';
|
||||||
|
documentMode?: boolean;
|
||||||
|
spaceId?: string;
|
||||||
|
}): Promise<Conversation | null> {
|
||||||
|
const { data, error } = await fetchApi<Conversation>('/conversations', {
|
||||||
|
method: 'POST',
|
||||||
|
body: options,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating conversation:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async addMessage(
|
||||||
|
conversationId: string,
|
||||||
|
sender: 'user' | 'assistant' | 'system',
|
||||||
|
messageText: string,
|
||||||
|
): Promise<Message | null> {
|
||||||
|
const { data, error } = await fetchApi<Message>(
|
||||||
|
`/conversations/${conversationId}/messages`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: { sender, messageText },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error adding message:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<Conversation>(
|
||||||
|
`/conversations/${conversationId}/title`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { title },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error updating title:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async archiveConversation(conversationId: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<Conversation>(
|
||||||
|
`/conversations/${conversationId}/archive`,
|
||||||
|
{ method: 'PATCH' },
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error archiving conversation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<Conversation>(
|
||||||
|
`/conversations/${conversationId}/unarchive`,
|
||||||
|
{ method: 'PATCH' },
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error unarchiving conversation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteConversation(conversationId: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<{ success: boolean }>(
|
||||||
|
`/conversations/${conversationId}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error deleting conversation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Template API ============
|
||||||
|
|
||||||
|
export type Template = {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion?: string;
|
||||||
|
modelId?: string;
|
||||||
|
color: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
documentMode: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const templateApi = {
|
||||||
|
async getTemplates(): Promise<Template[]> {
|
||||||
|
const { data, error } = await fetchApi<Template[]>('/templates');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading templates:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplate(id: string): Promise<Template | null> {
|
||||||
|
const { data, error } = await fetchApi<Template>(`/templates/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading template:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDefaultTemplate(): Promise<Template | null> {
|
||||||
|
const { data, error } = await fetchApi<Template | null>('/templates/default');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading default template:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTemplate(template: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion?: string;
|
||||||
|
modelId?: string;
|
||||||
|
color?: string;
|
||||||
|
documentMode?: boolean;
|
||||||
|
}): Promise<Template | null> {
|
||||||
|
const { data, error } = await fetchApi<Template>('/templates', {
|
||||||
|
method: 'POST',
|
||||||
|
body: template,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating template:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTemplate(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion: string;
|
||||||
|
modelId: string;
|
||||||
|
color: string;
|
||||||
|
documentMode: boolean;
|
||||||
|
}>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<Template>(`/templates/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: updates,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error updating template:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setDefaultTemplate(id: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<Template>(`/templates/${id}/default`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error setting default template:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<{ success: boolean }>(`/templates/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error deleting template:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Space API ============
|
||||||
|
|
||||||
|
export type Space = {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpaceMember = {
|
||||||
|
id: string;
|
||||||
|
spaceId: string;
|
||||||
|
userId: string;
|
||||||
|
role: 'owner' | 'admin' | 'member' | 'viewer';
|
||||||
|
invitationStatus: 'pending' | 'accepted' | 'declined';
|
||||||
|
invitedBy?: string;
|
||||||
|
invitedAt: string;
|
||||||
|
joinedAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const spaceApi = {
|
||||||
|
async getUserSpaces(): Promise<Space[]> {
|
||||||
|
const { data, error } = await fetchApi<Space[]>('/spaces');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading spaces:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOwnedSpaces(): Promise<Space[]> {
|
||||||
|
const { data, error } = await fetchApi<Space[]>('/spaces/owned');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading owned spaces:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSpace(id: string): Promise<Space | null> {
|
||||||
|
const { data, error } = await fetchApi<Space>(`/spaces/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading space:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||||
|
const { data, error } = await fetchApi<SpaceMember[]>(`/spaces/${spaceId}/members`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading space members:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserRoleInSpace(
|
||||||
|
spaceId: string,
|
||||||
|
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||||
|
const { data, error } = await fetchApi<{
|
||||||
|
role: 'owner' | 'admin' | 'member' | 'viewer' | null;
|
||||||
|
}>(`/spaces/${spaceId}/role`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error getting user role:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data?.role || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPendingInvitations(): Promise<
|
||||||
|
Array<{ invitation: SpaceMember; space: Space }>
|
||||||
|
> {
|
||||||
|
const { data, error } = await fetchApi<
|
||||||
|
Array<{ invitation: SpaceMember; space: Space }>
|
||||||
|
>('/spaces/invitations');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading invitations:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async createSpace(name: string, description?: string): Promise<Space | null> {
|
||||||
|
const { data, error } = await fetchApi<Space>('/spaces', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { name, description },
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating space:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateSpace(
|
||||||
|
id: string,
|
||||||
|
updates: { name?: string; description?: string; isArchived?: boolean },
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<Space>(`/spaces/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: updates,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error updating space:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteSpace(id: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<{ success: boolean }>(`/spaces/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error deleting space:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async inviteUser(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
role?: 'admin' | 'member' | 'viewer',
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<SpaceMember>(`/spaces/${spaceId}/invite`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId, role },
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error inviting user:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async respondToInvitation(
|
||||||
|
spaceId: string,
|
||||||
|
status: 'accepted' | 'declined',
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<SpaceMember>(`/spaces/${spaceId}/respond`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { status },
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error responding to invitation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<{ success: boolean }>(
|
||||||
|
`/spaces/${spaceId}/members/${userId}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error removing member:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async changeMemberRole(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
role: 'admin' | 'member' | 'viewer',
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<SpaceMember>(
|
||||||
|
`/spaces/${spaceId}/members/${userId}/role`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { role },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error changing member role:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Document API ============
|
||||||
|
|
||||||
|
export type Document = {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
version: number;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const documentApi = {
|
||||||
|
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||||
|
const { data, error } = await fetchApi<Document | null>(
|
||||||
|
`/documents/conversation/${conversationId}`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading document:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||||
|
const { data, error } = await fetchApi<Document[]>(
|
||||||
|
`/documents/conversation/${conversationId}/versions`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading document versions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async hasDocument(conversationId: string): Promise<boolean> {
|
||||||
|
const { data, error } = await fetchApi<{ exists: boolean }>(
|
||||||
|
`/documents/conversation/${conversationId}/exists`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error checking document:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return data?.exists || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDocument(
|
||||||
|
conversationId: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<Document | null> {
|
||||||
|
const { data, error } = await fetchApi<Document>(
|
||||||
|
`/documents/conversation/${conversationId}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: { content },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating document:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDocumentVersion(
|
||||||
|
conversationId: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<Document | null> {
|
||||||
|
const { data, error } = await fetchApi<Document>(
|
||||||
|
`/documents/conversation/${conversationId}/version`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: { content },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating document version:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||||
|
const { error } = await fetchApi<{ success: boolean }>(`/documents/${documentId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error deleting document:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Model API ============
|
||||||
|
|
||||||
|
export type Model = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
provider: string;
|
||||||
|
parameters?: {
|
||||||
|
deployment?: string;
|
||||||
|
temperature?: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
top_p?: number;
|
||||||
|
};
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modelApi = {
|
||||||
|
async getModels(): Promise<Model[]> {
|
||||||
|
const { data, error } = await fetchApi<Model[]>('/models');
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading models:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getModel(id: string): Promise<Model | null> {
|
||||||
|
const { data, error } = await fetchApi<Model>(`/models/${id}`);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading model:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Chat API ============
|
||||||
|
|
||||||
|
export type ChatMessage = {
|
||||||
|
role: 'system' | 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatCompletionResponse = {
|
||||||
|
content: string;
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chatApi = {
|
||||||
|
async createCompletion(options: {
|
||||||
|
messages: ChatMessage[];
|
||||||
|
modelId: string;
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
}): Promise<ChatCompletionResponse | null> {
|
||||||
|
const { data, error } = await fetchApi<ChatCompletionResponse>('/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
body: options,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating completion:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Chat Service - AI Completions via Backend
|
* Chat Service - AI Completions via Backend API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from './api';
|
import { chatApi, modelApi, type ChatMessage, type ChatCompletionResponse, type Model } from './api';
|
||||||
import type { ChatMessage, ChatCompletionResponse, AIModel } from '@chat/types';
|
|
||||||
|
export type { ChatMessage, ChatCompletionResponse };
|
||||||
|
|
||||||
export interface ChatCompletionRequest {
|
export interface ChatCompletionRequest {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
|
@ -16,31 +17,19 @@ export const chatService = {
|
||||||
/**
|
/**
|
||||||
* Get available AI models
|
* Get available AI models
|
||||||
*/
|
*/
|
||||||
async getModels(): Promise<AIModel[]> {
|
async getModels(): Promise<Model[]> {
|
||||||
const { data, error } = await api.get<AIModel[]>('/api/chat/models');
|
return modelApi.getModels();
|
||||||
if (error) {
|
|
||||||
console.error('Failed to fetch models:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return data || [];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send chat completion request
|
* Send chat completion request
|
||||||
*/
|
*/
|
||||||
async createCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse | null> {
|
async createCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse | null> {
|
||||||
const { data, error } = await api.post<ChatCompletionResponse>('/api/chat/completions', {
|
return chatApi.createCompletion({
|
||||||
messages: request.messages,
|
messages: request.messages,
|
||||||
modelId: request.modelId,
|
modelId: request.modelId,
|
||||||
temperature: request.temperature ?? 0.7,
|
temperature: request.temperature ?? 0.7,
|
||||||
maxTokens: request.maxTokens ?? 1000,
|
maxTokens: request.maxTokens ?? 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Chat completion failed:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data || null;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* Conversation Service - CRUD operations via Supabase
|
* Conversation Service - CRUD operations via Backend API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSupabaseBrowserClient } from './supabase';
|
import {
|
||||||
import { chatService } from './chat';
|
conversationApi,
|
||||||
import type { Conversation, Message, ChatMessage } from '@chat/types';
|
chatApi,
|
||||||
|
type Conversation,
|
||||||
|
type Message,
|
||||||
|
type ChatMessage,
|
||||||
|
} from './api';
|
||||||
|
|
||||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
export type { Conversation, Message };
|
||||||
|
|
||||||
function getSupabase() {
|
|
||||||
if (!supabase) {
|
|
||||||
supabase = createSupabaseBrowserClient();
|
|
||||||
}
|
|
||||||
return supabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const conversationService = {
|
export const conversationService = {
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,116 +22,45 @@ export const conversationService = {
|
||||||
mode: 'free' | 'guided' | 'template' = 'free',
|
mode: 'free' | 'guided' | 'template' = 'free',
|
||||||
templateId?: string,
|
templateId?: string,
|
||||||
documentMode: boolean = false,
|
documentMode: boolean = false,
|
||||||
spaceId?: string
|
spaceId?: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const sb = getSupabase();
|
const conversation = await conversationApi.createConversation({
|
||||||
|
modelId,
|
||||||
|
conversationMode: mode,
|
||||||
|
templateId,
|
||||||
|
documentMode,
|
||||||
|
spaceId,
|
||||||
|
});
|
||||||
|
|
||||||
const { data, error } = await sb
|
return conversation?.id || null;
|
||||||
.from('conversations')
|
|
||||||
.insert({
|
|
||||||
user_id: userId,
|
|
||||||
model_id: modelId,
|
|
||||||
template_id: templateId,
|
|
||||||
conversation_mode: mode,
|
|
||||||
document_mode: documentMode,
|
|
||||||
space_id: spaceId,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error creating conversation:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.id;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all active conversations for a user
|
* Get all active conversations for a user
|
||||||
*/
|
*/
|
||||||
async getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
async getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||||
const sb = getSupabase();
|
return conversationApi.getConversations(spaceId);
|
||||||
|
|
||||||
let query = sb
|
|
||||||
.from('conversations')
|
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_archived', false);
|
|
||||||
|
|
||||||
if (spaceId) {
|
|
||||||
query = query.eq('space_id', spaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await query.order('updated_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading conversations:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Conversation[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get archived conversations
|
* Get archived conversations
|
||||||
*/
|
*/
|
||||||
async getArchivedConversations(userId: string): Promise<Conversation[]> {
|
async getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||||
const sb = getSupabase();
|
return conversationApi.getArchivedConversations();
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('conversations')
|
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_archived', true)
|
|
||||||
.order('updated_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading archived conversations:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Conversation[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single conversation
|
* Get a single conversation
|
||||||
*/
|
*/
|
||||||
async getConversation(conversationId: string): Promise<Conversation | null> {
|
async getConversation(conversationId: string): Promise<Conversation | null> {
|
||||||
const sb = getSupabase();
|
return conversationApi.getConversation(conversationId);
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('conversations')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', conversationId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading conversation:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Conversation;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get messages for a conversation
|
* Get messages for a conversation
|
||||||
*/
|
*/
|
||||||
async getMessages(conversationId: string): Promise<Message[]> {
|
async getMessages(conversationId: string): Promise<Message[]> {
|
||||||
const sb = getSupabase();
|
return conversationApi.getMessages(conversationId);
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('messages')
|
|
||||||
.select('*')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('created_at', { ascending: true });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading messages:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Message[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -143,114 +69,38 @@ export const conversationService = {
|
||||||
async addMessage(
|
async addMessage(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
sender: 'user' | 'assistant' | 'system',
|
sender: 'user' | 'assistant' | 'system',
|
||||||
messageText: string
|
messageText: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const sb = getSupabase();
|
const message = await conversationApi.addMessage(conversationId, sender, messageText);
|
||||||
|
return message?.id || null;
|
||||||
const { data, error } = await sb
|
|
||||||
.from('messages')
|
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
sender,
|
|
||||||
message_text: messageText,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error adding message:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.id;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update conversation title
|
* Update conversation title
|
||||||
*/
|
*/
|
||||||
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return conversationApi.updateTitle(conversationId, title);
|
||||||
|
|
||||||
const { error } = await sb
|
|
||||||
.from('conversations')
|
|
||||||
.update({ title, updated_at: new Date().toISOString() })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error updating title:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Archive a conversation
|
* Archive a conversation
|
||||||
*/
|
*/
|
||||||
async archiveConversation(conversationId: string): Promise<boolean> {
|
async archiveConversation(conversationId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return conversationApi.archiveConversation(conversationId);
|
||||||
|
|
||||||
const { error } = await sb
|
|
||||||
.from('conversations')
|
|
||||||
.update({ is_archived: true })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error archiving conversation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unarchive a conversation
|
* Unarchive a conversation
|
||||||
*/
|
*/
|
||||||
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return conversationApi.unarchiveConversation(conversationId);
|
||||||
|
|
||||||
const { error } = await sb
|
|
||||||
.from('conversations')
|
|
||||||
.update({ is_archived: false })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error unarchiving conversation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a conversation permanently
|
* Delete a conversation permanently
|
||||||
*/
|
*/
|
||||||
async deleteConversation(conversationId: string): Promise<boolean> {
|
async deleteConversation(conversationId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return conversationApi.deleteConversation(conversationId);
|
||||||
|
|
||||||
// Delete messages first
|
|
||||||
const { error: messagesError } = await sb
|
|
||||||
.from('messages')
|
|
||||||
.delete()
|
|
||||||
.eq('conversation_id', conversationId);
|
|
||||||
|
|
||||||
if (messagesError) {
|
|
||||||
console.error('Error deleting messages:', messagesError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete conversation
|
|
||||||
const { error: conversationError } = await sb
|
|
||||||
.from('conversations')
|
|
||||||
.delete()
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
if (conversationError) {
|
|
||||||
console.error('Error deleting conversation:', conversationError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -259,7 +109,7 @@ export const conversationService = {
|
||||||
async sendMessageAndGetResponse(
|
async sendMessageAndGetResponse(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
modelId: string
|
modelId: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
userMessageId: string | null;
|
userMessageId: string | null;
|
||||||
assistantMessageId: string | null;
|
assistantMessageId: string | null;
|
||||||
|
|
@ -275,11 +125,11 @@ export const conversationService = {
|
||||||
// Build chat messages for API
|
// Build chat messages for API
|
||||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||||
role: m.sender === 'user' ? 'user' : m.sender === 'assistant' ? 'assistant' : 'system',
|
role: m.sender === 'user' ? 'user' : m.sender === 'assistant' ? 'assistant' : 'system',
|
||||||
content: m.message_text,
|
content: m.messageText,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Get AI response
|
// Get AI response
|
||||||
const response = await chatService.createCompletion({
|
const response = await chatApi.createCompletion({
|
||||||
messages: chatMessages,
|
messages: chatMessages,
|
||||||
modelId,
|
modelId,
|
||||||
});
|
});
|
||||||
|
|
@ -293,14 +143,11 @@ export const conversationService = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save assistant message
|
// Save assistant message
|
||||||
const assistantMessageId = await this.addMessage(conversationId, 'assistant', response.content);
|
const assistantMessageId = await this.addMessage(
|
||||||
|
conversationId,
|
||||||
// Update conversation timestamp
|
'assistant',
|
||||||
const sb = getSupabase();
|
response.content,
|
||||||
await sb
|
);
|
||||||
.from('conversations')
|
|
||||||
.update({ updated_at: new Date().toISOString() })
|
|
||||||
.eq('id', conversationId);
|
|
||||||
|
|
||||||
// Generate title if this is a new conversation (first or second message)
|
// Generate title if this is a new conversation (first or second message)
|
||||||
let title: string | undefined;
|
let title: string | undefined;
|
||||||
|
|
@ -325,7 +172,7 @@ export const conversationService = {
|
||||||
async generateTitle(userMessage: string): Promise<string> {
|
async generateTitle(userMessage: string): Promise<string> {
|
||||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für diesen Chat: "${userMessage}"`;
|
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für diesen Chat: "${userMessage}"`;
|
||||||
|
|
||||||
const response = await chatService.createCompletion({
|
const response = await chatApi.createCompletion({
|
||||||
messages: [{ role: 'user', content: titlePrompt }],
|
messages: [{ role: 'user', content: titlePrompt }],
|
||||||
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,40 @@
|
||||||
/**
|
/**
|
||||||
* Document Service - Manage documents in document mode conversations
|
* Document Service - CRUD operations via Backend API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSupabaseBrowserClient } from './supabase';
|
import { documentApi, conversationApi, type Document } from './api';
|
||||||
import type { Document, DocumentWithConversation } from '@chat/types';
|
|
||||||
|
|
||||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
export type { Document };
|
||||||
|
|
||||||
function getSupabase() {
|
export type DocumentWithConversation = Document & {
|
||||||
if (!supabase) {
|
conversation_title: string;
|
||||||
supabase = createSupabaseBrowserClient();
|
};
|
||||||
}
|
|
||||||
return supabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const documentService = {
|
export const documentService = {
|
||||||
/**
|
/**
|
||||||
* Get all documents for a user (latest version of each)
|
* Get all documents for a user (latest version of each)
|
||||||
|
* This requires fetching conversations first, then documents
|
||||||
*/
|
*/
|
||||||
async getUserDocuments(userId: string): Promise<DocumentWithConversation[]> {
|
async getUserDocuments(userId: string): Promise<DocumentWithConversation[]> {
|
||||||
const sb = getSupabase();
|
// Get all conversations (the API will filter by user)
|
||||||
|
const conversations = await conversationApi.getConversations();
|
||||||
|
|
||||||
// Get all conversations with document_mode enabled
|
// Filter to only document mode conversations
|
||||||
const { data: conversations, error: convError } = await sb
|
const documentConversations = conversations.filter((c) => c.documentMode);
|
||||||
.from('conversations')
|
|
||||||
.select('id, title, document_mode')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('document_mode', true);
|
|
||||||
|
|
||||||
if (convError) {
|
if (documentConversations.length === 0) {
|
||||||
console.error('Error loading conversations:', convError);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conversations || conversations.length === 0) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each conversation, load the latest document version
|
// For each conversation, load the latest document version
|
||||||
const documents: DocumentWithConversation[] = [];
|
const documents: DocumentWithConversation[] = [];
|
||||||
|
|
||||||
for (const conv of conversations) {
|
for (const conv of documentConversations) {
|
||||||
const { data: docData, error: docError } = await sb
|
const doc = await documentApi.getLatestDocument(conv.id);
|
||||||
.from('documents')
|
|
||||||
.select('*')
|
|
||||||
.eq('conversation_id', conv.id)
|
|
||||||
.order('version', { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (docError && docError.code !== 'PGRST116') {
|
if (doc) {
|
||||||
console.error(`Error loading document for conversation ${conv.id}:`, docError);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (docData) {
|
|
||||||
documents.push({
|
documents.push({
|
||||||
...docData,
|
...doc,
|
||||||
conversation_title: conv.title || 'Unbenannte Konversation',
|
conversation_title: conv.title || 'Unbenannte Konversation',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -69,108 +47,44 @@ export const documentService = {
|
||||||
* Get the latest document for a conversation
|
* Get the latest document for a conversation
|
||||||
*/
|
*/
|
||||||
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||||
const sb = getSupabase();
|
return documentApi.getLatestDocument(conversationId);
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('documents')
|
|
||||||
.select('*')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('version', { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
if (error.code !== 'PGRST116') {
|
|
||||||
console.error('Error loading document:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Document;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new document
|
* Create a new document
|
||||||
*/
|
*/
|
||||||
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
||||||
const sb = getSupabase();
|
return documentApi.createDocument(conversationId, content);
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('documents')
|
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
version: 1,
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error creating document:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Document;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new version of a document
|
* Create a new version of a document
|
||||||
*/
|
*/
|
||||||
async createDocumentVersion(conversationId: string, content: string): Promise<Document | null> {
|
async createDocumentVersion(
|
||||||
const sb = getSupabase();
|
conversationId: string,
|
||||||
|
content: string,
|
||||||
// Get the current highest version
|
): Promise<Document | null> {
|
||||||
const { data: latestVersionData, error: versionError } = await sb
|
return documentApi.createDocumentVersion(conversationId, content);
|
||||||
.from('documents')
|
|
||||||
.select('version')
|
|
||||||
.eq('conversation_id', conversationId)
|
|
||||||
.order('version', { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (versionError && versionError.code !== 'PGRST116') {
|
|
||||||
console.error('Error loading latest document version:', versionError);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newVersion = (latestVersionData?.version || 0) + 1;
|
|
||||||
|
|
||||||
// Create a new document version
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('documents')
|
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
version: newVersion,
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error creating document version:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Document;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all versions of a document
|
* Get all versions of a document
|
||||||
*/
|
*/
|
||||||
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||||
const sb = getSupabase();
|
return documentApi.getAllDocumentVersions(conversationId);
|
||||||
|
},
|
||||||
|
|
||||||
const { data, error } = await sb
|
/**
|
||||||
.from('documents')
|
* Check if a document exists for a conversation
|
||||||
.select('*')
|
*/
|
||||||
.eq('conversation_id', conversationId)
|
async hasDocument(conversationId: string): Promise<boolean> {
|
||||||
.order('version', { ascending: false });
|
return documentApi.hasDocument(conversationId);
|
||||||
|
},
|
||||||
|
|
||||||
if (error) {
|
/**
|
||||||
console.error('Error loading document versions:', error);
|
* Delete a specific document version
|
||||||
return [];
|
*/
|
||||||
}
|
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||||
|
return documentApi.deleteDocumentVersion(documentId);
|
||||||
return data as Document[];
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,151 +1,60 @@
|
||||||
/**
|
/**
|
||||||
* Space Service - CRUD operations via Supabase
|
* Space Service - CRUD operations via Backend API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSupabaseBrowserClient } from './supabase';
|
import { spaceApi, type Space, type SpaceMember } from './api';
|
||||||
import type { Space, SpaceMember, SpaceCreate, SpaceUpdate } from '@chat/types';
|
|
||||||
|
|
||||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
export type { Space, SpaceMember };
|
||||||
|
|
||||||
function getSupabase() {
|
|
||||||
if (!supabase) {
|
|
||||||
supabase = createSupabaseBrowserClient();
|
|
||||||
}
|
|
||||||
return supabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const spaceService = {
|
export const spaceService = {
|
||||||
/**
|
/**
|
||||||
* Get all spaces for a user (both owned and member of)
|
* Get all spaces for the current user (both owned and member of)
|
||||||
*/
|
*/
|
||||||
async getUserSpaces(userId: string): Promise<Space[]> {
|
async getUserSpaces(userId: string): Promise<Space[]> {
|
||||||
const sb = getSupabase();
|
return spaceApi.getUserSpaces();
|
||||||
|
|
||||||
// Get space IDs the user is a member of (with accepted status)
|
|
||||||
const { data: memberData, error: memberError } = await sb
|
|
||||||
.from('space_members')
|
|
||||||
.select('space_id')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('invitation_status', 'accepted');
|
|
||||||
|
|
||||||
if (memberError) {
|
|
||||||
console.error('Error fetching user space memberships:', memberError);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!memberData || memberData.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const spaceIds = memberData.map((m) => m.space_id);
|
|
||||||
|
|
||||||
// Fetch the actual space data
|
|
||||||
const { data: spaces, error: spacesError } = await sb
|
|
||||||
.from('spaces')
|
|
||||||
.select('*')
|
|
||||||
.in('id', spaceIds)
|
|
||||||
.eq('is_archived', false)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (spacesError) {
|
|
||||||
console.error('Error fetching spaces:', spacesError);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return spaces as Space[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single space by ID
|
* Get a single space by ID
|
||||||
*/
|
*/
|
||||||
async getSpace(spaceId: string): Promise<Space | null> {
|
async getSpace(spaceId: string): Promise<Space | null> {
|
||||||
const sb = getSupabase();
|
return spaceApi.getSpace(spaceId);
|
||||||
|
|
||||||
const { data, error } = await sb.from('spaces').select('*').eq('id', spaceId).single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching space:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Space;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new space
|
* Create a new space
|
||||||
*/
|
*/
|
||||||
async createSpace(space: SpaceCreate): Promise<string | null> {
|
async createSpace(space: {
|
||||||
const sb = getSupabase();
|
name: string;
|
||||||
|
description?: string;
|
||||||
const { data, error } = await sb
|
owner_id: string;
|
||||||
.from('spaces')
|
}): Promise<string | null> {
|
||||||
.insert({
|
const result = await spaceApi.createSpace(space.name, space.description);
|
||||||
name: space.name,
|
return result?.id || null;
|
||||||
description: space.description,
|
|
||||||
owner_id: space.owner_id,
|
|
||||||
})
|
|
||||||
.select('id')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error creating space:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.id;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a space
|
* Update a space
|
||||||
*/
|
*/
|
||||||
async updateSpace(spaceId: string, updates: SpaceUpdate): Promise<boolean> {
|
async updateSpace(
|
||||||
const sb = getSupabase();
|
spaceId: string,
|
||||||
|
updates: { name?: string; description?: string; isArchived?: boolean },
|
||||||
const { error } = await sb.from('spaces').update(updates).eq('id', spaceId);
|
): Promise<boolean> {
|
||||||
|
return spaceApi.updateSpace(spaceId, updates);
|
||||||
if (error) {
|
|
||||||
console.error('Error updating space:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a space
|
* Delete a space
|
||||||
*/
|
*/
|
||||||
async deleteSpace(spaceId: string): Promise<boolean> {
|
async deleteSpace(spaceId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return spaceApi.deleteSpace(spaceId);
|
||||||
|
|
||||||
const { error } = await sb.from('spaces').delete().eq('id', spaceId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error deleting space:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get members of a space
|
* Get members of a space
|
||||||
*/
|
*/
|
||||||
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||||
const sb = getSupabase();
|
return spaceApi.getSpaceMembers(spaceId);
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('space_members')
|
|
||||||
.select('*')
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.order('role', { ascending: true })
|
|
||||||
.order('joined_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching space members:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as SpaceMember[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -153,62 +62,63 @@ export const spaceService = {
|
||||||
*/
|
*/
|
||||||
async getUserRoleInSpace(
|
async getUserRoleInSpace(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
userId: string
|
userId: string,
|
||||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||||
const sb = getSupabase();
|
return spaceApi.getUserRoleInSpace(spaceId);
|
||||||
|
|
||||||
// First check if they're the owner
|
|
||||||
const { data: space, error: spaceError } = await sb
|
|
||||||
.from('spaces')
|
|
||||||
.select('owner_id')
|
|
||||||
.eq('id', spaceId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (spaceError) {
|
|
||||||
console.error('Error checking space ownership:', spaceError);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (space.owner_id === userId) {
|
|
||||||
return 'owner';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not owner, check membership
|
|
||||||
const { data: member, error: memberError } = await sb
|
|
||||||
.from('space_members')
|
|
||||||
.select('role, invitation_status')
|
|
||||||
.eq('space_id', spaceId)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (memberError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member && member.invitation_status === 'accepted') {
|
|
||||||
return member.role as 'admin' | 'member' | 'viewer';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leave a space
|
* Leave a space (remove self from members)
|
||||||
*/
|
*/
|
||||||
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return spaceApi.removeMember(spaceId, userId);
|
||||||
|
},
|
||||||
|
|
||||||
const { error } = await sb
|
/**
|
||||||
.from('space_members')
|
* Invite a user to a space
|
||||||
.delete()
|
*/
|
||||||
.eq('space_id', spaceId)
|
async inviteUserToSpace(
|
||||||
.eq('user_id', userId);
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
role: 'admin' | 'member' | 'viewer' = 'member',
|
||||||
|
): Promise<boolean> {
|
||||||
|
return spaceApi.inviteUser(spaceId, userId, role);
|
||||||
|
},
|
||||||
|
|
||||||
if (error) {
|
/**
|
||||||
console.error('Error leaving space:', error);
|
* Respond to a space invitation
|
||||||
return false;
|
*/
|
||||||
}
|
async respondToInvitation(
|
||||||
|
spaceId: string,
|
||||||
|
status: 'accepted' | 'declined',
|
||||||
|
): Promise<boolean> {
|
||||||
|
return spaceApi.respondToInvitation(spaceId, status);
|
||||||
|
},
|
||||||
|
|
||||||
return true;
|
/**
|
||||||
|
* Get pending invitations for the current user
|
||||||
|
*/
|
||||||
|
async getPendingInvitations(): Promise<
|
||||||
|
Array<{ invitation: SpaceMember; space: Space }>
|
||||||
|
> {
|
||||||
|
return spaceApi.getPendingInvitations();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a member from a space
|
||||||
|
*/
|
||||||
|
async removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||||
|
return spaceApi.removeMember(spaceId, userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a member's role
|
||||||
|
*/
|
||||||
|
async changeMemberRole(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
newRole: 'admin' | 'member' | 'viewer',
|
||||||
|
): Promise<boolean> {
|
||||||
|
return spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,157 +1,86 @@
|
||||||
/**
|
/**
|
||||||
* Template Service - CRUD operations via Supabase
|
* Template Service - CRUD operations via Backend API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSupabaseBrowserClient } from './supabase';
|
import { templateApi, type Template } from './api';
|
||||||
import type { Template, TemplateCreate, TemplateUpdate } from '@chat/types';
|
|
||||||
|
|
||||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
export type { Template };
|
||||||
|
|
||||||
function getSupabase() {
|
|
||||||
if (!supabase) {
|
|
||||||
supabase = createSupabaseBrowserClient();
|
|
||||||
}
|
|
||||||
return supabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const templateService = {
|
export const templateService = {
|
||||||
/**
|
/**
|
||||||
* Get all templates for a user
|
* Get all templates for the current user
|
||||||
*/
|
*/
|
||||||
async getTemplates(userId: string): Promise<Template[]> {
|
async getTemplates(userId: string): Promise<Template[]> {
|
||||||
const sb = getSupabase();
|
return templateApi.getTemplates();
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('templates')
|
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.order('name');
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading templates:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Template[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single template by ID
|
* Get a single template by ID
|
||||||
*/
|
*/
|
||||||
async getTemplate(templateId: string): Promise<Template | null> {
|
async getTemplate(templateId: string): Promise<Template | null> {
|
||||||
const sb = getSupabase();
|
return templateApi.getTemplate(templateId);
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('templates')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', templateId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading template:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Template;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default template for a user
|
* Get the default template for the current user
|
||||||
*/
|
*/
|
||||||
async getDefaultTemplate(userId: string): Promise<Template | null> {
|
async getDefaultTemplate(userId: string): Promise<Template | null> {
|
||||||
const sb = getSupabase();
|
return templateApi.getDefaultTemplate();
|
||||||
|
|
||||||
const { data, error } = await sb
|
|
||||||
.from('templates')
|
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_default', true)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error loading default template:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data as Template;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new template
|
* Create a new template
|
||||||
*/
|
*/
|
||||||
async createTemplate(template: TemplateCreate): Promise<Template | null> {
|
async createTemplate(template: {
|
||||||
const sb = getSupabase();
|
userId: string;
|
||||||
|
name: string;
|
||||||
const { data, error } = await sb
|
description?: string;
|
||||||
.from('templates')
|
systemPrompt: string;
|
||||||
.insert(template)
|
initialQuestion?: string;
|
||||||
.select()
|
modelId?: string;
|
||||||
.single();
|
color?: string;
|
||||||
|
documentMode?: boolean;
|
||||||
if (error) {
|
}): Promise<Template | null> {
|
||||||
console.error('Error creating template:', error);
|
return templateApi.createTemplate({
|
||||||
return null;
|
name: template.name,
|
||||||
}
|
description: template.description,
|
||||||
|
systemPrompt: template.systemPrompt,
|
||||||
return data as Template;
|
initialQuestion: template.initialQuestion,
|
||||||
|
modelId: template.modelId,
|
||||||
|
color: template.color,
|
||||||
|
documentMode: template.documentMode,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a template
|
* Update an existing template
|
||||||
*/
|
*/
|
||||||
async updateTemplate(templateId: string, updates: TemplateUpdate): Promise<boolean> {
|
async updateTemplate(
|
||||||
const sb = getSupabase();
|
templateId: string,
|
||||||
|
updates: Partial<{
|
||||||
const { error } = await sb
|
name: string;
|
||||||
.from('templates')
|
description: string;
|
||||||
.update(updates)
|
systemPrompt: string;
|
||||||
.eq('id', templateId);
|
initialQuestion: string;
|
||||||
|
modelId: string;
|
||||||
if (error) {
|
color: string;
|
||||||
console.error('Error updating template:', error);
|
documentMode: boolean;
|
||||||
return false;
|
}>,
|
||||||
}
|
): Promise<boolean> {
|
||||||
|
return templateApi.updateTemplate(templateId, updates);
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a template
|
* Delete a template
|
||||||
*/
|
*/
|
||||||
async deleteTemplate(templateId: string): Promise<boolean> {
|
async deleteTemplate(templateId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return templateApi.deleteTemplate(templateId);
|
||||||
|
|
||||||
const { error } = await sb.from('templates').delete().eq('id', templateId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error deleting template:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a template as default
|
* Set a template as default
|
||||||
*/
|
*/
|
||||||
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||||
const sb = getSupabase();
|
return templateApi.setDefaultTemplate(templateId);
|
||||||
|
|
||||||
// First, unset all defaults for this user
|
|
||||||
await sb.from('templates').update({ is_default: false }).eq('user_id', userId);
|
|
||||||
|
|
||||||
// Then set the selected template as default
|
|
||||||
const { error } = await sb
|
|
||||||
.from('templates')
|
|
||||||
.update({ is_default: true })
|
|
||||||
.eq('id', templateId)
|
|
||||||
.eq('user_id', userId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error setting default template:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,26 @@
|
||||||
* Now using Mana Core Auth instead of Supabase Auth
|
* Now using Mana Core Auth instead of Supabase Auth
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||||
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||||
|
|
||||||
// Initialize Mana Core Auth
|
// Initialize Mana Core Auth only on the client side
|
||||||
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||||
const { authService, tokenManager } = initializeWebAuth({
|
|
||||||
baseUrl: MANA_AUTH_URL,
|
// Lazy initialization to avoid SSR issues with localStorage
|
||||||
});
|
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||||
|
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||||
|
|
||||||
|
function getAuthService() {
|
||||||
|
if (!browser) return null;
|
||||||
|
if (!_authService) {
|
||||||
|
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||||
|
_authService = auth.authService;
|
||||||
|
_tokenManager = auth.tokenManager;
|
||||||
|
}
|
||||||
|
return _authService;
|
||||||
|
}
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let user = $state<UserData | null>(null);
|
let user = $state<UserData | null>(null);
|
||||||
|
|
@ -38,6 +50,13 @@ export const authStore = {
|
||||||
async initialize() {
|
async initialize() {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
|
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
initialized = true;
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const authenticated = await authService.isAuthenticated();
|
const authenticated = await authService.isAuthenticated();
|
||||||
|
|
@ -58,6 +77,11 @@ export const authStore = {
|
||||||
* Sign in with email and password
|
* Sign in with email and password
|
||||||
*/
|
*/
|
||||||
async signIn(email: string, password: string) {
|
async signIn(email: string, password: string) {
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
return { success: false, error: 'Auth not available on server' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await authService.signIn(email, password);
|
const result = await authService.signIn(email, password);
|
||||||
|
|
||||||
|
|
@ -80,6 +104,11 @@ export const authStore = {
|
||||||
* Sign up with email and password
|
* Sign up with email and password
|
||||||
*/
|
*/
|
||||||
async signUp(email: string, password: string) {
|
async signUp(email: string, password: string) {
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
return { success: false, error: 'Auth not available on server', needsVerification: false };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await authService.signUp(email, password);
|
const result = await authService.signUp(email, password);
|
||||||
|
|
||||||
|
|
@ -105,6 +134,12 @@ export const authStore = {
|
||||||
* Sign out
|
* Sign out
|
||||||
*/
|
*/
|
||||||
async signOut() {
|
async signOut() {
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
user = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await authService.signOut();
|
await authService.signOut();
|
||||||
user = null;
|
user = null;
|
||||||
|
|
@ -119,6 +154,11 @@ export const authStore = {
|
||||||
* Send password reset email
|
* Send password reset email
|
||||||
*/
|
*/
|
||||||
async resetPassword(email: string) {
|
async resetPassword(email: string) {
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
return { success: false, error: 'Auth not available on server' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await authService.forgotPassword(email);
|
const result = await authService.forgotPassword(email);
|
||||||
|
|
||||||
|
|
@ -137,6 +177,11 @@ export const authStore = {
|
||||||
* Get user credit balance
|
* Get user credit balance
|
||||||
*/
|
*/
|
||||||
async getCredits() {
|
async getCredits() {
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credits = await authService.getUserCredits();
|
const credits = await authService.getUserCredits();
|
||||||
return credits;
|
return credits;
|
||||||
|
|
@ -150,6 +195,10 @@ export const authStore = {
|
||||||
* Get access token for API calls
|
* Get access token for API calls
|
||||||
*/
|
*/
|
||||||
async getAccessToken() {
|
async getAccessToken() {
|
||||||
|
const authService = getAuthService();
|
||||||
|
if (!authService) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return await authService.getAppToken();
|
return await authService.getAppToken();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||||
import { ChatLogo } from '@manacore/shared-branding';
|
import { ChatLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
|
||||||
|
// Get redirect URL from query params
|
||||||
|
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/chat');
|
||||||
|
|
||||||
// German translations
|
// German translations
|
||||||
const translations = {
|
const translations = {
|
||||||
title: 'Anmelden',
|
title: 'Anmelden',
|
||||||
|
|
@ -47,7 +51,7 @@
|
||||||
goto={goto}
|
goto={goto}
|
||||||
enableGoogle={false}
|
enableGoogle={false}
|
||||||
enableApple={false}
|
enableApple={false}
|
||||||
successRedirect="/chat"
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
lightBackground="#e0f2fe"
|
lightBackground="#e0f2fe"
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* Protected routes layout server
|
* Protected routes layout server
|
||||||
* Validates session and redirects to login if not authenticated
|
* Auth checking is now done client-side via Mana Core Auth
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals, url }) => {
|
export const load: LayoutServerLoad = async ({ url }) => {
|
||||||
const { session, user } = await locals.safeGetSession();
|
// Return the current path for client-side redirect logic
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
// Redirect to login with return URL
|
|
||||||
const redirectTo = encodeURIComponent(url.pathname);
|
|
||||||
redirect(303, `/login?redirectTo=${redirectTo}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
pathname: url.pathname,
|
||||||
user,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,19 @@
|
||||||
|
|
||||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||||
|
|
||||||
// Set session from server data
|
let isChecking = $state(true);
|
||||||
onMount(() => {
|
|
||||||
if (data.session) {
|
// Check auth on mount and redirect if not authenticated
|
||||||
authStore.setSession(data.session);
|
onMount(async () => {
|
||||||
|
await authStore.initialize();
|
||||||
|
|
||||||
|
if (!authStore.isAuthenticated) {
|
||||||
|
const redirectTo = encodeURIComponent(data.pathname || '/chat');
|
||||||
|
goto(`/login?redirectTo=${redirectTo}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isChecking = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSignOut() {
|
async function handleSignOut() {
|
||||||
|
|
@ -20,6 +28,12 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if isChecking}
|
||||||
|
<!-- Loading state while checking auth -->
|
||||||
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"></div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<!-- Top Navigation -->
|
<!-- Top Navigation -->
|
||||||
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
|
@ -79,9 +93,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
{#if data.user}
|
{#if authStore.user}
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
|
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
|
||||||
{data.user.email}
|
{authStore.user.email}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
|
|
@ -110,3 +124,4 @@
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
37
chat/backend/.dockerignore
Normal file
37
chat/backend/.dockerignore
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
20
chat/backend/.env.docker
Normal file
20
chat/backend/.env.docker
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Docker Environment Configuration
|
||||||
|
# Copy this file to .env and fill in the values
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_USER=chat
|
||||||
|
DB_PASSWORD=chatpassword
|
||||||
|
DB_NAME=chat
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Azure OpenAI Configuration (required)
|
||||||
|
AZURE_OPENAI_ENDPOINT=https://your-azure-openai-endpoint.openai.azure.com
|
||||||
|
AZURE_OPENAI_API_KEY=your-api-key-here
|
||||||
|
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||||
|
|
||||||
|
# Mana Core Auth URL
|
||||||
|
# Use host.docker.internal to connect to services running on host machine
|
||||||
|
MANA_CORE_AUTH_URL=http://host.docker.internal:3001
|
||||||
|
|
||||||
|
# Backend Port (exposed on host)
|
||||||
|
BACKEND_PORT=3002
|
||||||
|
|
@ -6,9 +6,8 @@ AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||||
# Mana Core Auth Configuration
|
# Mana Core Auth Configuration
|
||||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||||
|
|
||||||
# Supabase Configuration (for database only, not auth)
|
# PostgreSQL Database Configuration
|
||||||
SUPABASE_URL=https://your-project.supabase.co
|
DATABASE_URL=postgresql://chat:password@localhost:5432/chat
|
||||||
SUPABASE_SERVICE_KEY=your-service-key-here
|
|
||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
PORT=3002
|
PORT=3002
|
||||||
|
|
|
||||||
63
chat/backend/Dockerfile
Normal file
63
chat/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy root workspace files
|
||||||
|
COPY pnpm-workspace.yaml ./
|
||||||
|
COPY package.json ./
|
||||||
|
COPY pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Copy shared packages
|
||||||
|
COPY packages/shared-errors ./packages/shared-errors
|
||||||
|
|
||||||
|
# Copy chat backend
|
||||||
|
COPY chat/backend ./chat/backend
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Build shared packages first
|
||||||
|
WORKDIR /app/packages/shared-errors
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# Build the backend
|
||||||
|
WORKDIR /app/chat/backend
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
# Install pnpm and postgresql-client for health checks
|
||||||
|
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||||
|
&& apk add --no-cache postgresql-client
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy everything from builder (including node_modules)
|
||||||
|
COPY --from=builder /app/pnpm-workspace.yaml ./
|
||||||
|
COPY --from=builder /app/package.json ./
|
||||||
|
COPY --from=builder /app/pnpm-lock.yaml ./
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/packages ./packages
|
||||||
|
COPY --from=builder /app/chat/backend ./chat/backend
|
||||||
|
|
||||||
|
# Copy entrypoint script
|
||||||
|
COPY chat/backend/docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
WORKDIR /app/chat/backend
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3002
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3002/api/health || exit 1
|
||||||
|
|
||||||
|
# Run entrypoint script
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
67
chat/backend/docker-compose.yml
Normal file
67
chat/backend/docker-compose.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: chat-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-chat}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-chatpassword}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-chat}
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-5433}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init-db:/docker-entrypoint-initdb.d
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-chat} -d ${DB_NAME:-chat}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Chat Backend API
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: chat/backend/Dockerfile
|
||||||
|
container_name: chat-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
# Database
|
||||||
|
DATABASE_URL: postgresql://${DB_USER:-chat}:${DB_PASSWORD:-chatpassword}@postgres:5432/${DB_NAME:-chat}
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: ${DB_USER:-chat}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-chatpassword}
|
||||||
|
DB_NAME: ${DB_NAME:-chat}
|
||||||
|
|
||||||
|
# Azure OpenAI
|
||||||
|
AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT}
|
||||||
|
AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY}
|
||||||
|
AZURE_OPENAI_API_VERSION: ${AZURE_OPENAI_API_VERSION:-2024-12-01-preview}
|
||||||
|
|
||||||
|
# Mana Core Auth
|
||||||
|
MANA_CORE_AUTH_URL: ${MANA_CORE_AUTH_URL:-http://host.docker.internal:3001}
|
||||||
|
|
||||||
|
# Server
|
||||||
|
PORT: 3002
|
||||||
|
NODE_ENV: production
|
||||||
|
ports:
|
||||||
|
- "${BACKEND_PORT:-3002}:3002"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3002/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 30s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: chat-network
|
||||||
34
chat/backend/docker-entrypoint.sh
Normal file
34
chat/backend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Chat Backend Entrypoint ==="
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-chat} 2>/dev/null; do
|
||||||
|
echo "PostgreSQL is unavailable - sleeping"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "PostgreSQL is up!"
|
||||||
|
|
||||||
|
cd /app/chat/backend
|
||||||
|
|
||||||
|
# Run schema push (for development) or migrations (for production)
|
||||||
|
if [ "$NODE_ENV" = "production" ] && [ -d "src/db/migrations/meta" ]; then
|
||||||
|
echo "Running database migrations..."
|
||||||
|
npx tsx src/db/migrate.ts
|
||||||
|
echo "Migrations completed!"
|
||||||
|
else
|
||||||
|
echo "Pushing database schema (development mode)..."
|
||||||
|
npx drizzle-kit push --force
|
||||||
|
echo "Schema push completed!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run seed (only seeds if data doesn't exist)
|
||||||
|
echo "Running database seed..."
|
||||||
|
npx tsx src/db/seed.ts
|
||||||
|
echo "Seed completed!"
|
||||||
|
|
||||||
|
# Execute the main command
|
||||||
|
echo "Starting application..."
|
||||||
|
exec "$@"
|
||||||
12
chat/backend/drizzle.config.ts
Normal file
12
chat/backend/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: 'postgresql',
|
||||||
|
schema: './src/db/schema/index.ts',
|
||||||
|
out: './src/db/migrations',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL || 'postgresql://chat:password@localhost:5432/chat',
|
||||||
|
},
|
||||||
|
verbose: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true
|
"deleteOutDir": true,
|
||||||
|
"assets": [],
|
||||||
|
"watchAssets": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,18 @@
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit",
|
||||||
|
"migration:generate": "drizzle-kit generate",
|
||||||
|
"migration:run": "tsx src/db/migrate.ts",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:seed": "tsx src/db/seed.ts",
|
||||||
|
"docker:build": "docker compose build",
|
||||||
|
"docker:up": "docker compose up -d",
|
||||||
|
"docker:down": "docker compose down",
|
||||||
|
"docker:logs": "docker compose logs -f",
|
||||||
|
"docker:restart": "docker compose restart",
|
||||||
|
"docker:clean": "docker compose down -v --rmi local"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@manacore/shared-errors": "workspace:*",
|
"@manacore/shared-errors": "workspace:*",
|
||||||
|
|
@ -18,10 +29,13 @@
|
||||||
"@nestjs/config": "^3.3.0",
|
"@nestjs/config": "^3.3.0",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^10.4.15",
|
||||||
"@nestjs/platform-express": "^10.4.15",
|
"@nestjs/platform-express": "^10.4.15",
|
||||||
"@supabase/supabase-js": "^2.47.10",
|
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"drizzle-kit": "^0.30.2",
|
||||||
|
"drizzle-orm": "^0.38.3",
|
||||||
"openai": "^4.77.0",
|
"openai": "^4.77.0",
|
||||||
|
"postgres": "^3.4.5",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
|
@ -40,6 +54,7 @@
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { DatabaseModule } from './db/database.module';
|
||||||
import { ChatModule } from './chat/chat.module';
|
import { ChatModule } from './chat/chat.module';
|
||||||
import { ConversationModule } from './conversation/conversation.module';
|
import { ConversationModule } from './conversation/conversation.module';
|
||||||
|
import { TemplateModule } from './template/template.module';
|
||||||
|
import { SpaceModule } from './space/space.module';
|
||||||
|
import { DocumentModule } from './document/document.module';
|
||||||
|
import { ModelModule } from './model/model.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
@ -10,8 +15,13 @@ import { HealthModule } from './health/health.module';
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
envFilePath: '.env',
|
envFilePath: '.env',
|
||||||
}),
|
}),
|
||||||
|
DatabaseModule,
|
||||||
ChatModule,
|
ChatModule,
|
||||||
ConversationModule,
|
ConversationModule,
|
||||||
|
TemplateModule,
|
||||||
|
SpaceModule,
|
||||||
|
DocumentModule,
|
||||||
|
ModelModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export class ChatController {
|
||||||
constructor(private readonly chatService: ChatService) {}
|
constructor(private readonly chatService: ChatService) {}
|
||||||
|
|
||||||
@Get('models')
|
@Get('models')
|
||||||
getModels() {
|
async getModels() {
|
||||||
return this.chatService.getAvailableModels();
|
return this.chatService.getAvailableModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
import {
|
import {
|
||||||
type AsyncResult,
|
type AsyncResult,
|
||||||
ok,
|
ok,
|
||||||
|
|
@ -7,22 +8,11 @@ import {
|
||||||
ValidationError,
|
ValidationError,
|
||||||
ServiceError,
|
ServiceError,
|
||||||
} from '@manacore/shared-errors';
|
} from '@manacore/shared-errors';
|
||||||
|
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||||
|
import { type Database } from '../db/connection';
|
||||||
|
import { models, type Model } from '../db/schema/models.schema';
|
||||||
import { ChatCompletionDto, ChatCompletionResponseDto } from './dto/chat-completion.dto';
|
import { ChatCompletionDto, ChatCompletionResponseDto } from './dto/chat-completion.dto';
|
||||||
|
|
||||||
export interface AIModel {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
parameters: {
|
|
||||||
temperature: number;
|
|
||||||
max_tokens: number;
|
|
||||||
provider: string;
|
|
||||||
deployment: string;
|
|
||||||
endpoint: string;
|
|
||||||
api_version: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChatService {
|
export class ChatService {
|
||||||
private readonly logger = new Logger(ChatService.name);
|
private readonly logger = new Logger(ChatService.name);
|
||||||
|
|
@ -30,72 +20,55 @@ export class ChatService {
|
||||||
private readonly endpoint: string;
|
private readonly endpoint: string;
|
||||||
private readonly apiVersion: string;
|
private readonly apiVersion: string;
|
||||||
|
|
||||||
// Available models configuration
|
constructor(
|
||||||
private readonly availableModels: AIModel[] = [
|
private configService: ConfigService,
|
||||||
{
|
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
) {
|
||||||
name: 'GPT-O3-Mini',
|
|
||||||
description: 'Azure OpenAI O3-Mini: Effizientes Modell für schnelle Antworten.',
|
|
||||||
parameters: {
|
|
||||||
temperature: 0.7,
|
|
||||||
max_tokens: 800,
|
|
||||||
provider: 'azure',
|
|
||||||
deployment: 'gpt-o3-mini-se',
|
|
||||||
endpoint: 'https://memoroseopenai.openai.azure.com',
|
|
||||||
api_version: '2024-12-01-preview',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '550e8400-e29b-41d4-a716-446655440004',
|
|
||||||
name: 'GPT-4o-Mini',
|
|
||||||
description: 'Azure OpenAI GPT-4o-Mini: Kompaktes, leistungsstarkes KI-Modell.',
|
|
||||||
parameters: {
|
|
||||||
temperature: 0.7,
|
|
||||||
max_tokens: 1000,
|
|
||||||
provider: 'azure',
|
|
||||||
deployment: 'gpt-4o-mini-se',
|
|
||||||
endpoint: 'https://memoroseopenai.openai.azure.com',
|
|
||||||
api_version: '2024-12-01-preview',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '550e8400-e29b-41d4-a716-446655440005',
|
|
||||||
name: 'GPT-4o',
|
|
||||||
description: 'Azure OpenAI GPT-4o: Das fortschrittlichste multimodale KI-Modell.',
|
|
||||||
parameters: {
|
|
||||||
temperature: 0.7,
|
|
||||||
max_tokens: 1200,
|
|
||||||
provider: 'azure',
|
|
||||||
deployment: 'gpt-4o-se',
|
|
||||||
endpoint: 'https://memoroseopenai.openai.azure.com',
|
|
||||||
api_version: '2024-12-01-preview',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
|
||||||
this.apiKey = this.configService.get<string>('AZURE_OPENAI_API_KEY') || '';
|
this.apiKey = this.configService.get<string>('AZURE_OPENAI_API_KEY') || '';
|
||||||
this.endpoint = this.configService.get<string>('AZURE_OPENAI_ENDPOINT') || 'https://memoroseopenai.openai.azure.com';
|
this.endpoint =
|
||||||
this.apiVersion = this.configService.get<string>('AZURE_OPENAI_API_VERSION') || '2024-12-01-preview';
|
this.configService.get<string>('AZURE_OPENAI_ENDPOINT') ||
|
||||||
|
'https://memoroseopenai.openai.azure.com';
|
||||||
|
this.apiVersion =
|
||||||
|
this.configService.get<string>('AZURE_OPENAI_API_VERSION') ||
|
||||||
|
'2024-12-01-preview';
|
||||||
|
|
||||||
if (!this.apiKey) {
|
if (!this.apiKey) {
|
||||||
this.logger.warn('AZURE_OPENAI_API_KEY is not set!');
|
this.logger.warn('AZURE_OPENAI_API_KEY is not set!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailableModels(): AIModel[] {
|
async getAvailableModels(): Promise<Model[]> {
|
||||||
return this.availableModels;
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(models)
|
||||||
|
.where(eq(models.isActive, true));
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching models from database', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getModelById(modelId: string): AIModel | undefined {
|
async getModelById(modelId: string): Promise<Model | undefined> {
|
||||||
return this.availableModels.find((m) => m.id === modelId);
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(models)
|
||||||
|
.where(eq(models.id, modelId))
|
||||||
|
.limit(1);
|
||||||
|
return result[0];
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching model from database', error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCompletion(
|
async createCompletion(
|
||||||
dto: ChatCompletionDto,
|
dto: ChatCompletionDto,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): AsyncResult<ChatCompletionResponseDto> {
|
): AsyncResult<ChatCompletionResponseDto> {
|
||||||
const model = this.getModelById(dto.modelId);
|
const model = await this.getModelById(dto.modelId);
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return err(
|
return err(
|
||||||
|
|
@ -110,9 +83,15 @@ export class ChatService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deployment = model.parameters.deployment;
|
const params = model.parameters as {
|
||||||
const temperature = dto.temperature ?? model.parameters.temperature;
|
deployment?: string;
|
||||||
const maxTokens = dto.maxTokens ?? model.parameters.max_tokens;
|
temperature?: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
const deployment = params?.deployment || 'gpt-4o-mini-se';
|
||||||
|
const temperature = dto.temperature ?? params?.temperature ?? 0.7;
|
||||||
|
const maxTokens = dto.maxTokens ?? params?.max_tokens ?? 1000;
|
||||||
|
|
||||||
// Prepare request body
|
// Prepare request body
|
||||||
const requestBody: Record<string, unknown> = {
|
const requestBody: Record<string, unknown> = {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
|
||||||
import { isOk } from '@manacore/shared-errors';
|
|
||||||
import {
|
import {
|
||||||
ConversationService,
|
Body,
|
||||||
type Conversation,
|
Controller,
|
||||||
type Message,
|
Delete,
|
||||||
} from './conversation.service';
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { isOk } from '@manacore/shared-errors';
|
||||||
|
import { ConversationService } from './conversation.service';
|
||||||
|
import { type Conversation } from '../db/schema/conversations.schema';
|
||||||
|
import { type Message } from '../db/schema/messages.schema';
|
||||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||||
import {
|
import {
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
|
|
@ -19,8 +27,27 @@ export class ConversationController {
|
||||||
@Get()
|
@Get()
|
||||||
async getConversations(
|
async getConversations(
|
||||||
@CurrentUser() user: CurrentUserData,
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@Query('spaceId') spaceId?: string,
|
||||||
): Promise<Conversation[]> {
|
): Promise<Conversation[]> {
|
||||||
const result = await this.conversationService.getConversations(user.userId);
|
const result = await this.conversationService.getConversations(
|
||||||
|
user.userId,
|
||||||
|
spaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('archived')
|
||||||
|
async getArchivedConversations(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Conversation[]> {
|
||||||
|
const result = await this.conversationService.getArchivedConversations(
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isOk(result)) {
|
if (!isOk(result)) {
|
||||||
throw result.error;
|
throw result.error;
|
||||||
|
|
@ -34,8 +61,10 @@ export class ConversationController {
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@CurrentUser() user: CurrentUserData,
|
@CurrentUser() user: CurrentUserData,
|
||||||
): Promise<Conversation> {
|
): Promise<Conversation> {
|
||||||
// TODO: Add ownership check - ensure conversation belongs to user
|
const result = await this.conversationService.getConversation(
|
||||||
const result = await this.conversationService.getConversation(id);
|
id,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isOk(result)) {
|
if (!isOk(result)) {
|
||||||
throw result.error;
|
throw result.error;
|
||||||
|
|
@ -49,8 +78,7 @@ export class ConversationController {
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@CurrentUser() user: CurrentUserData,
|
@CurrentUser() user: CurrentUserData,
|
||||||
): Promise<Message[]> {
|
): Promise<Message[]> {
|
||||||
// TODO: Add ownership check - ensure conversation belongs to user
|
const result = await this.conversationService.getMessages(id, user.userId);
|
||||||
const result = await this.conversationService.getMessages(id);
|
|
||||||
|
|
||||||
if (!isOk(result)) {
|
if (!isOk(result)) {
|
||||||
throw result.error;
|
throw result.error;
|
||||||
|
|
@ -61,13 +89,27 @@ export class ConversationController {
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async createConversation(
|
async createConversation(
|
||||||
@Body() body: { modelId: string; title?: string },
|
@Body()
|
||||||
|
body: {
|
||||||
|
modelId: string;
|
||||||
|
title?: string;
|
||||||
|
templateId?: string;
|
||||||
|
conversationMode?: 'free' | 'guided' | 'template';
|
||||||
|
documentMode?: boolean;
|
||||||
|
spaceId?: string;
|
||||||
|
},
|
||||||
@CurrentUser() user: CurrentUserData,
|
@CurrentUser() user: CurrentUserData,
|
||||||
): Promise<Conversation> {
|
): Promise<Conversation> {
|
||||||
const result = await this.conversationService.createConversation(
|
const result = await this.conversationService.createConversation(
|
||||||
user.userId,
|
user.userId,
|
||||||
body.modelId,
|
body.modelId,
|
||||||
body.title,
|
{
|
||||||
|
title: body.title,
|
||||||
|
templateId: body.templateId,
|
||||||
|
conversationMode: body.conversationMode,
|
||||||
|
documentMode: body.documentMode,
|
||||||
|
spaceId: body.spaceId,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isOk(result)) {
|
if (!isOk(result)) {
|
||||||
|
|
@ -83,9 +125,9 @@ export class ConversationController {
|
||||||
@Body() body: { sender: 'user' | 'assistant' | 'system'; messageText: string },
|
@Body() body: { sender: 'user' | 'assistant' | 'system'; messageText: string },
|
||||||
@CurrentUser() user: CurrentUserData,
|
@CurrentUser() user: CurrentUserData,
|
||||||
): Promise<Message> {
|
): Promise<Message> {
|
||||||
// TODO: Add ownership check - ensure conversation belongs to user
|
|
||||||
const result = await this.conversationService.addMessage(
|
const result = await this.conversationService.addMessage(
|
||||||
id,
|
id,
|
||||||
|
user.userId,
|
||||||
body.sender,
|
body.sender,
|
||||||
body.messageText,
|
body.messageText,
|
||||||
);
|
);
|
||||||
|
|
@ -96,4 +138,74 @@ export class ConversationController {
|
||||||
|
|
||||||
return result.value;
|
return result.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id/title')
|
||||||
|
async updateTitle(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { title: string },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Conversation> {
|
||||||
|
const result = await this.conversationService.updateTitle(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
body.title,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/archive')
|
||||||
|
async archiveConversation(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Conversation> {
|
||||||
|
const result = await this.conversationService.archiveConversation(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/unarchive')
|
||||||
|
async unarchiveConversation(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Conversation> {
|
||||||
|
const result = await this.conversationService.unarchiveConversation(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteConversation(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const result = await this.conversationService.deleteConversation(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,168 +1,319 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { eq, and, desc, asc, sql } from 'drizzle-orm';
|
||||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
import {
|
import {
|
||||||
type AsyncResult,
|
type AsyncResult,
|
||||||
ok,
|
ok,
|
||||||
err,
|
err,
|
||||||
ServiceError,
|
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
} from '@manacore/shared-errors';
|
} from '@manacore/shared-errors';
|
||||||
|
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||||
export interface Conversation {
|
import { type Database } from '../db/connection';
|
||||||
id: string;
|
import {
|
||||||
user_id: string;
|
conversations,
|
||||||
model_id: string;
|
type Conversation,
|
||||||
title?: string;
|
type NewConversation,
|
||||||
is_archived: boolean;
|
} from '../db/schema/conversations.schema';
|
||||||
created_at: string;
|
import { messages, type Message, type NewMessage } from '../db/schema/messages.schema';
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
id: string;
|
|
||||||
conversation_id: string;
|
|
||||||
sender: 'user' | 'assistant' | 'system';
|
|
||||||
message_text: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConversationService {
|
export class ConversationService {
|
||||||
private readonly logger = new Logger(ConversationService.name);
|
private readonly logger = new Logger(ConversationService.name);
|
||||||
private supabase: SupabaseClient | null = null;
|
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(
|
||||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL');
|
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||||
const supabaseKey = this.configService.get<string>('SUPABASE_SERVICE_KEY');
|
) {}
|
||||||
|
|
||||||
if (supabaseUrl && supabaseKey) {
|
async getConversations(
|
||||||
this.supabase = createClient(supabaseUrl, supabaseKey);
|
userId: string,
|
||||||
} else {
|
spaceId?: string,
|
||||||
this.logger.warn('Supabase configuration missing');
|
): AsyncResult<Conversation[]> {
|
||||||
}
|
try {
|
||||||
|
const conditions = [
|
||||||
|
eq(conversations.userId, userId),
|
||||||
|
eq(conversations.isArchived, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (spaceId) {
|
||||||
|
conditions.push(eq(conversations.spaceId, spaceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConversations(userId: string): AsyncResult<Conversation[]> {
|
const result = await this.db
|
||||||
if (!this.supabase) {
|
.select()
|
||||||
return err(ServiceError.unavailable('Database'));
|
.from(conversations)
|
||||||
}
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(conversations.updatedAt));
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
return ok(result);
|
||||||
.from('conversations')
|
} catch (error) {
|
||||||
.select('*')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_archived', false)
|
|
||||||
.order('updated_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
this.logger.error('Error fetching conversations', error);
|
this.logger.error('Error fetching conversations', error);
|
||||||
return err(DatabaseError.queryFailed('Failed to fetch conversations'));
|
return err(DatabaseError.queryFailed('Failed to fetch conversations'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok(data || []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConversation(id: string): AsyncResult<Conversation> {
|
async getArchivedConversations(userId: string): AsyncResult<Conversation[]> {
|
||||||
if (!this.supabase) {
|
try {
|
||||||
return err(ServiceError.unavailable('Database'));
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(conversations)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(conversations.userId, userId),
|
||||||
|
eq(conversations.isArchived, true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(desc(conversations.updatedAt));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching archived conversations', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch archived conversations'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
async getConversation(id: string, userId: string): AsyncResult<Conversation> {
|
||||||
.from('conversations')
|
try {
|
||||||
.select('*')
|
const result = await this.db
|
||||||
.eq('id', id)
|
.select()
|
||||||
.single();
|
.from(conversations)
|
||||||
|
.where(
|
||||||
|
and(eq(conversations.id, id), eq(conversations.userId, userId)),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (error) {
|
if (result.length === 0) {
|
||||||
this.logger.error('Error fetching conversation', error);
|
|
||||||
if (error.code === 'PGRST116') {
|
|
||||||
return err(new NotFoundError('Conversation', id));
|
return err(new NotFoundError('Conversation', id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching conversation', error);
|
||||||
return err(DatabaseError.queryFailed('Failed to fetch conversation'));
|
return err(DatabaseError.queryFailed('Failed to fetch conversation'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMessages(conversationId: string): AsyncResult<Message[]> {
|
async getMessages(
|
||||||
if (!this.supabase) {
|
conversationId: string,
|
||||||
return err(ServiceError.unavailable('Database'));
|
userId: string,
|
||||||
|
): AsyncResult<Message[]> {
|
||||||
|
try {
|
||||||
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.getConversation(conversationId, userId);
|
||||||
|
if (!convResult.ok) {
|
||||||
|
return err(convResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const result = await this.db
|
||||||
.from('messages')
|
.select()
|
||||||
.select('*')
|
.from(messages)
|
||||||
.eq('conversation_id', conversationId)
|
.where(eq(messages.conversationId, conversationId))
|
||||||
.order('created_at', { ascending: true });
|
.orderBy(asc(messages.createdAt));
|
||||||
|
|
||||||
if (error) {
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
this.logger.error('Error fetching messages', error);
|
this.logger.error('Error fetching messages', error);
|
||||||
return err(DatabaseError.queryFailed('Failed to fetch messages'));
|
return err(DatabaseError.queryFailed('Failed to fetch messages'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok(data || []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createConversation(
|
async createConversation(
|
||||||
userId: string,
|
userId: string,
|
||||||
modelId: string,
|
modelId: string,
|
||||||
title?: string,
|
options?: {
|
||||||
|
title?: string;
|
||||||
|
templateId?: string;
|
||||||
|
conversationMode?: 'free' | 'guided' | 'template';
|
||||||
|
documentMode?: boolean;
|
||||||
|
spaceId?: string;
|
||||||
|
},
|
||||||
): AsyncResult<Conversation> {
|
): AsyncResult<Conversation> {
|
||||||
if (!this.supabase) {
|
try {
|
||||||
return err(ServiceError.unavailable('Database'));
|
const newConversation: NewConversation = {
|
||||||
}
|
userId,
|
||||||
|
modelId,
|
||||||
|
title: options?.title || 'Neue Unterhaltung',
|
||||||
|
templateId: options?.templateId,
|
||||||
|
conversationMode: options?.conversationMode || 'free',
|
||||||
|
documentMode: options?.documentMode || false,
|
||||||
|
spaceId: options?.spaceId,
|
||||||
|
isArchived: false,
|
||||||
|
};
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const result = await this.db
|
||||||
.from('conversations')
|
.insert(conversations)
|
||||||
.insert({
|
.values(newConversation)
|
||||||
user_id: userId,
|
.returning();
|
||||||
model_id: modelId,
|
|
||||||
title: title || 'Neue Unterhaltung',
|
|
||||||
is_archived: false,
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
this.logger.error('Error creating conversation', error);
|
this.logger.error('Error creating conversation', error);
|
||||||
return err(DatabaseError.queryFailed('Failed to create conversation'));
|
return err(DatabaseError.queryFailed('Failed to create conversation'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMessage(
|
async addMessage(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
sender: 'user' | 'assistant' | 'system',
|
sender: 'user' | 'assistant' | 'system',
|
||||||
messageText: string,
|
messageText: string,
|
||||||
): AsyncResult<Message> {
|
): AsyncResult<Message> {
|
||||||
if (!this.supabase) {
|
try {
|
||||||
return err(ServiceError.unavailable('Database'));
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.getConversation(conversationId, userId);
|
||||||
|
if (!convResult.ok) {
|
||||||
|
return err(convResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const newMessage: NewMessage = {
|
||||||
.from('messages')
|
conversationId,
|
||||||
.insert({
|
|
||||||
conversation_id: conversationId,
|
|
||||||
sender,
|
sender,
|
||||||
message_text: messageText,
|
messageText,
|
||||||
})
|
};
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
const result = await this.db
|
||||||
|
.insert(messages)
|
||||||
|
.values(newMessage)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Update conversation updated_at
|
||||||
|
await this.db
|
||||||
|
.update(conversations)
|
||||||
|
.set({ updatedAt: new Date() })
|
||||||
|
.where(eq(conversations.id, conversationId));
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
this.logger.error('Error adding message', error);
|
this.logger.error('Error adding message', error);
|
||||||
return err(DatabaseError.queryFailed('Failed to add message'));
|
return err(DatabaseError.queryFailed('Failed to add message'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update conversation updated_at
|
async updateTitle(
|
||||||
await this.supabase
|
conversationId: string,
|
||||||
.from('conversations')
|
userId: string,
|
||||||
.update({ updated_at: new Date().toISOString() })
|
title: string,
|
||||||
.eq('id', conversationId);
|
): AsyncResult<Conversation> {
|
||||||
|
try {
|
||||||
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.getConversation(conversationId, userId);
|
||||||
|
if (!convResult.ok) {
|
||||||
|
return err(convResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
return ok(data);
|
const result = await this.db
|
||||||
|
.update(conversations)
|
||||||
|
.set({ title, updatedAt: new Date() })
|
||||||
|
.where(eq(conversations.id, conversationId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error updating title', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to update title'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveConversation(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<Conversation> {
|
||||||
|
try {
|
||||||
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.getConversation(conversationId, userId);
|
||||||
|
if (!convResult.ok) {
|
||||||
|
return err(convResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.update(conversations)
|
||||||
|
.set({ isArchived: true, updatedAt: new Date() })
|
||||||
|
.where(eq(conversations.id, conversationId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error archiving conversation', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to archive conversation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unarchiveConversation(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<Conversation> {
|
||||||
|
try {
|
||||||
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.db
|
||||||
|
.select()
|
||||||
|
.from(conversations)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(conversations.id, conversationId),
|
||||||
|
eq(conversations.userId, userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (convResult.length === 0) {
|
||||||
|
return err(new NotFoundError('Conversation', conversationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.update(conversations)
|
||||||
|
.set({ isArchived: false, updatedAt: new Date() })
|
||||||
|
.where(eq(conversations.id, conversationId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error unarchiving conversation', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to unarchive conversation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteConversation(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<void> {
|
||||||
|
try {
|
||||||
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.getConversation(conversationId, userId);
|
||||||
|
if (!convResult.ok) {
|
||||||
|
return err(convResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages will be cascade deleted due to foreign key constraint
|
||||||
|
await this.db
|
||||||
|
.delete(conversations)
|
||||||
|
.where(eq(conversations.id, conversationId));
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error deleting conversation', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to delete conversation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessageCount(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<number> {
|
||||||
|
try {
|
||||||
|
// First verify the conversation belongs to the user
|
||||||
|
const convResult = await this.getConversation(conversationId, userId);
|
||||||
|
if (!convResult.ok) {
|
||||||
|
return err(convResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(messages)
|
||||||
|
.where(eq(messages.conversationId, conversationId));
|
||||||
|
|
||||||
|
return ok(Number(result[0]?.count || 0));
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error getting message count', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to get message count'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
chat/backend/src/db/connection.ts
Normal file
38
chat/backend/src/db/connection.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import * as schema from './schema';
|
||||||
|
|
||||||
|
// Use require for postgres to avoid ESM/CommonJS interop issues
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const postgres = require('postgres');
|
||||||
|
|
||||||
|
let connection: ReturnType<typeof postgres> | null = null;
|
||||||
|
let db: ReturnType<typeof drizzle> | null = null;
|
||||||
|
|
||||||
|
export function getConnection(databaseUrl: string) {
|
||||||
|
if (!connection) {
|
||||||
|
connection = postgres(databaseUrl, {
|
||||||
|
max: 10,
|
||||||
|
idle_timeout: 20,
|
||||||
|
connect_timeout: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDb(databaseUrl: string) {
|
||||||
|
if (!db) {
|
||||||
|
const conn = getConnection(databaseUrl);
|
||||||
|
db = drizzle(conn, { schema });
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeConnection() {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
connection = null;
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Database = ReturnType<typeof getDb>;
|
||||||
28
chat/backend/src/db/database.module.ts
Normal file
28
chat/backend/src/db/database.module.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Module, Global, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { getDb, closeConnection, type Database } from './connection';
|
||||||
|
|
||||||
|
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DATABASE_CONNECTION,
|
||||||
|
useFactory: (configService: ConfigService): Database => {
|
||||||
|
const databaseUrl = configService.get<string>('DATABASE_URL');
|
||||||
|
if (!databaseUrl) {
|
||||||
|
throw new Error('DATABASE_URL environment variable is not set');
|
||||||
|
}
|
||||||
|
return getDb(databaseUrl);
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [DATABASE_CONNECTION],
|
||||||
|
})
|
||||||
|
export class DatabaseModule implements OnModuleDestroy {
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await closeConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
chat/backend/src/db/migrate.ts
Normal file
29
chat/backend/src/db/migrate.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||||
|
import { getDb, closeConnection } from './connection';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
async function runMigrations() {
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
|
||||||
|
if (!databaseUrl) {
|
||||||
|
throw new Error('DATABASE_URL environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Running migrations...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDb(databaseUrl);
|
||||||
|
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
||||||
|
console.log('Migrations completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await closeConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runMigrations();
|
||||||
43
chat/backend/src/db/schema/conversations.schema.ts
Normal file
43
chat/backend/src/db/schema/conversations.schema.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { pgTable, uuid, text, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { messages } from './messages.schema';
|
||||||
|
import { documents } from './documents.schema';
|
||||||
|
import { spaces } from './spaces.schema';
|
||||||
|
import { models } from './models.schema';
|
||||||
|
import { templates } from './templates.schema';
|
||||||
|
|
||||||
|
export const conversationModeEnum = pgEnum('conversation_mode', ['free', 'guided', 'template']);
|
||||||
|
|
||||||
|
export const conversations = pgTable('conversations', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
userId: uuid('user_id').notNull(),
|
||||||
|
modelId: uuid('model_id').references(() => models.id),
|
||||||
|
templateId: uuid('template_id').references(() => templates.id),
|
||||||
|
spaceId: uuid('space_id').references(() => spaces.id, { onDelete: 'set null' }),
|
||||||
|
title: text('title'),
|
||||||
|
conversationMode: conversationModeEnum('conversation_mode').default('free').notNull(),
|
||||||
|
documentMode: boolean('document_mode').default(false).notNull(),
|
||||||
|
isArchived: boolean('is_archived').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const conversationsRelations = relations(conversations, ({ one, many }) => ({
|
||||||
|
model: one(models, {
|
||||||
|
fields: [conversations.modelId],
|
||||||
|
references: [models.id],
|
||||||
|
}),
|
||||||
|
template: one(templates, {
|
||||||
|
fields: [conversations.templateId],
|
||||||
|
references: [templates.id],
|
||||||
|
}),
|
||||||
|
space: one(spaces, {
|
||||||
|
fields: [conversations.spaceId],
|
||||||
|
references: [spaces.id],
|
||||||
|
}),
|
||||||
|
messages: many(messages),
|
||||||
|
documents: many(documents),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type Conversation = typeof conversations.$inferSelect;
|
||||||
|
export type NewConversation = typeof conversations.$inferInsert;
|
||||||
24
chat/backend/src/db/schema/documents.schema.ts
Normal file
24
chat/backend/src/db/schema/documents.schema.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { conversations } from './conversations.schema';
|
||||||
|
|
||||||
|
export const documents = pgTable('documents', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
conversationId: uuid('conversation_id')
|
||||||
|
.references(() => conversations.id, { onDelete: 'cascade' })
|
||||||
|
.notNull(),
|
||||||
|
version: integer('version').default(1).notNull(),
|
||||||
|
content: text('content').notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const documentsRelations = relations(documents, ({ one }) => ({
|
||||||
|
conversation: one(conversations, {
|
||||||
|
fields: [documents.conversationId],
|
||||||
|
references: [conversations.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type Document = typeof documents.$inferSelect;
|
||||||
|
export type NewDocument = typeof documents.$inferInsert;
|
||||||
7
chat/backend/src/db/schema/index.ts
Normal file
7
chat/backend/src/db/schema/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './conversations.schema';
|
||||||
|
export * from './messages.schema';
|
||||||
|
export * from './models.schema';
|
||||||
|
export * from './templates.schema';
|
||||||
|
export * from './spaces.schema';
|
||||||
|
export * from './documents.schema';
|
||||||
|
export * from './usage-logs.schema';
|
||||||
26
chat/backend/src/db/schema/messages.schema.ts
Normal file
26
chat/backend/src/db/schema/messages.schema.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { pgTable, uuid, text, timestamp, pgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { conversations } from './conversations.schema';
|
||||||
|
|
||||||
|
export const senderEnum = pgEnum('sender', ['user', 'assistant', 'system']);
|
||||||
|
|
||||||
|
export const messages = pgTable('messages', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
conversationId: uuid('conversation_id')
|
||||||
|
.references(() => conversations.id, { onDelete: 'cascade' })
|
||||||
|
.notNull(),
|
||||||
|
sender: senderEnum('sender').notNull(),
|
||||||
|
messageText: text('message_text').notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const messagesRelations = relations(messages, ({ one }) => ({
|
||||||
|
conversation: one(conversations, {
|
||||||
|
fields: [messages.conversationId],
|
||||||
|
references: [conversations.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type Message = typeof messages.$inferSelect;
|
||||||
|
export type NewMessage = typeof messages.$inferInsert;
|
||||||
20
chat/backend/src/db/schema/models.schema.ts
Normal file
20
chat/backend/src/db/schema/models.schema.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { pgTable, uuid, text, timestamp, jsonb, boolean } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
export const models = pgTable('models', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
provider: text('provider').notNull(), // 'azure', 'openai', 'anthropic', etc.
|
||||||
|
parameters: jsonb('parameters').$type<{
|
||||||
|
deployment?: string;
|
||||||
|
temperature?: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
top_p?: number;
|
||||||
|
}>(),
|
||||||
|
isActive: boolean('is_active').default(true).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Model = typeof models.$inferSelect;
|
||||||
|
export type NewModel = typeof models.$inferInsert;
|
||||||
46
chat/backend/src/db/schema/spaces.schema.ts
Normal file
46
chat/backend/src/db/schema/spaces.schema.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { pgTable, uuid, text, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export const memberRoleEnum = pgEnum('member_role', ['owner', 'admin', 'member', 'viewer']);
|
||||||
|
export const invitationStatusEnum = pgEnum('invitation_status', ['pending', 'accepted', 'declined']);
|
||||||
|
|
||||||
|
export const spaces = pgTable('spaces', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
ownerId: uuid('owner_id').notNull(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
isArchived: boolean('is_archived').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const spaceMembers = pgTable('space_members', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
spaceId: uuid('space_id')
|
||||||
|
.references(() => spaces.id, { onDelete: 'cascade' })
|
||||||
|
.notNull(),
|
||||||
|
userId: uuid('user_id').notNull(),
|
||||||
|
role: memberRoleEnum('role').default('member').notNull(),
|
||||||
|
invitationStatus: invitationStatusEnum('invitation_status').default('pending').notNull(),
|
||||||
|
invitedBy: uuid('invited_by'),
|
||||||
|
invitedAt: timestamp('invited_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
joinedAt: timestamp('joined_at', { withTimezone: true }),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const spacesRelations = relations(spaces, ({ many }) => ({
|
||||||
|
members: many(spaceMembers),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const spaceMembersRelations = relations(spaceMembers, ({ one }) => ({
|
||||||
|
space: one(spaces, {
|
||||||
|
fields: [spaceMembers.spaceId],
|
||||||
|
references: [spaces.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type Space = typeof spaces.$inferSelect;
|
||||||
|
export type NewSpace = typeof spaces.$inferInsert;
|
||||||
|
export type SpaceMember = typeof spaceMembers.$inferSelect;
|
||||||
|
export type NewSpaceMember = typeof spaceMembers.$inferInsert;
|
||||||
28
chat/backend/src/db/schema/templates.schema.ts
Normal file
28
chat/backend/src/db/schema/templates.schema.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { pgTable, uuid, text, timestamp, boolean } from 'drizzle-orm/pg-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { models } from './models.schema';
|
||||||
|
|
||||||
|
export const templates = pgTable('templates', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
userId: uuid('user_id').notNull(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
systemPrompt: text('system_prompt').notNull(),
|
||||||
|
initialQuestion: text('initial_question'),
|
||||||
|
modelId: uuid('model_id').references(() => models.id),
|
||||||
|
color: text('color').default('#3b82f6').notNull(),
|
||||||
|
isDefault: boolean('is_default').default(false).notNull(),
|
||||||
|
documentMode: boolean('document_mode').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const templatesRelations = relations(templates, ({ one }) => ({
|
||||||
|
model: one(models, {
|
||||||
|
fields: [templates.modelId],
|
||||||
|
references: [models.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type Template = typeof templates.$inferSelect;
|
||||||
|
export type NewTemplate = typeof templates.$inferInsert;
|
||||||
40
chat/backend/src/db/schema/usage-logs.schema.ts
Normal file
40
chat/backend/src/db/schema/usage-logs.schema.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { pgTable, uuid, timestamp, integer, numeric } from 'drizzle-orm/pg-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { conversations } from './conversations.schema';
|
||||||
|
import { messages } from './messages.schema';
|
||||||
|
import { models } from './models.schema';
|
||||||
|
|
||||||
|
export const usageLogs = pgTable('usage_logs', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
conversationId: uuid('conversation_id')
|
||||||
|
.references(() => conversations.id, { onDelete: 'cascade' })
|
||||||
|
.notNull(),
|
||||||
|
messageId: uuid('message_id')
|
||||||
|
.references(() => messages.id, { onDelete: 'cascade' })
|
||||||
|
.notNull(),
|
||||||
|
userId: uuid('user_id').notNull(),
|
||||||
|
modelId: uuid('model_id').references(() => models.id),
|
||||||
|
promptTokens: integer('prompt_tokens').default(0).notNull(),
|
||||||
|
completionTokens: integer('completion_tokens').default(0).notNull(),
|
||||||
|
totalTokens: integer('total_tokens').default(0).notNull(),
|
||||||
|
estimatedCost: numeric('estimated_cost', { precision: 10, scale: 6 }).default('0'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usageLogsRelations = relations(usageLogs, ({ one }) => ({
|
||||||
|
conversation: one(conversations, {
|
||||||
|
fields: [usageLogs.conversationId],
|
||||||
|
references: [conversations.id],
|
||||||
|
}),
|
||||||
|
message: one(messages, {
|
||||||
|
fields: [usageLogs.messageId],
|
||||||
|
references: [messages.id],
|
||||||
|
}),
|
||||||
|
model: one(models, {
|
||||||
|
fields: [usageLogs.modelId],
|
||||||
|
references: [models.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type UsageLog = typeof usageLogs.$inferSelect;
|
||||||
|
export type NewUsageLog = typeof usageLogs.$inferInsert;
|
||||||
100
chat/backend/src/db/seed.ts
Normal file
100
chat/backend/src/db/seed.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* Database Seed Script
|
||||||
|
* Seeds initial data for the chat application
|
||||||
|
*/
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
import { models } from './schema';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL || 'postgresql://chat:password@localhost:5432/chat';
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
console.log('Starting database seed...');
|
||||||
|
|
||||||
|
const client = postgres(connectionString);
|
||||||
|
const db = drizzle(client);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if models already exist
|
||||||
|
const existingModels = await db.select().from(models);
|
||||||
|
|
||||||
|
if (existingModels.length > 0) {
|
||||||
|
console.log(`Found ${existingModels.length} existing models. Skipping seed.`);
|
||||||
|
await client.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed AI models
|
||||||
|
console.log('Seeding AI models...');
|
||||||
|
|
||||||
|
const modelData = [
|
||||||
|
{
|
||||||
|
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||||
|
name: 'GPT-O3-Mini',
|
||||||
|
description: 'Fast, efficient responses for everyday tasks',
|
||||||
|
provider: 'azure',
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 800,
|
||||||
|
deployment: 'gpt-o3-mini-se',
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '550e8400-e29b-41d4-a716-446655440004',
|
||||||
|
name: 'GPT-4o-Mini',
|
||||||
|
description: 'Compact and powerful for complex tasks',
|
||||||
|
provider: 'azure',
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 1000,
|
||||||
|
deployment: 'gpt-4o-mini-se',
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '550e8400-e29b-41d4-a716-446655440005',
|
||||||
|
name: 'GPT-4o',
|
||||||
|
description: 'Most advanced model for demanding tasks',
|
||||||
|
provider: 'azure',
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 2000,
|
||||||
|
deployment: 'gpt-4o-se',
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await db.insert(models).values(modelData);
|
||||||
|
|
||||||
|
console.log(`Seeded ${modelData.length} AI models successfully!`);
|
||||||
|
|
||||||
|
// Log the seeded models
|
||||||
|
const seededModels = await db.select().from(models);
|
||||||
|
console.log('Seeded models:');
|
||||||
|
seededModels.forEach((model) => {
|
||||||
|
console.log(` - ${model.name} (${model.id})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error seeding database:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run seed
|
||||||
|
seed()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Seed completed!');
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Seed failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
129
chat/backend/src/document/document.controller.ts
Normal file
129
chat/backend/src/document/document.controller.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { isOk } from '@manacore/shared-errors';
|
||||||
|
import { DocumentService } from './document.service';
|
||||||
|
import { type Document } from '../db/schema/documents.schema';
|
||||||
|
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||||
|
import {
|
||||||
|
CurrentUser,
|
||||||
|
CurrentUserData,
|
||||||
|
} from '../common/decorators/current-user.decorator';
|
||||||
|
|
||||||
|
@Controller('documents')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class DocumentController {
|
||||||
|
constructor(private readonly documentService: DocumentService) {}
|
||||||
|
|
||||||
|
@Get('conversation/:conversationId')
|
||||||
|
async getLatestDocument(
|
||||||
|
@Param('conversationId') conversationId: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Document | null> {
|
||||||
|
const result = await this.documentService.getLatestDocument(
|
||||||
|
conversationId,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('conversation/:conversationId/versions')
|
||||||
|
async getAllDocumentVersions(
|
||||||
|
@Param('conversationId') conversationId: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Document[]> {
|
||||||
|
const result = await this.documentService.getAllDocumentVersions(
|
||||||
|
conversationId,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('conversation/:conversationId/exists')
|
||||||
|
async hasDocument(
|
||||||
|
@Param('conversationId') conversationId: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ exists: boolean }> {
|
||||||
|
const result = await this.documentService.hasDocument(
|
||||||
|
conversationId,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: result.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('conversation/:conversationId')
|
||||||
|
async createDocument(
|
||||||
|
@Param('conversationId') conversationId: string,
|
||||||
|
@Body() body: { content: string },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Document> {
|
||||||
|
const result = await this.documentService.createDocument(
|
||||||
|
conversationId,
|
||||||
|
user.userId,
|
||||||
|
body.content,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('conversation/:conversationId/version')
|
||||||
|
async createDocumentVersion(
|
||||||
|
@Param('conversationId') conversationId: string,
|
||||||
|
@Body() body: { content: string },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Document> {
|
||||||
|
const result = await this.documentService.createDocumentVersion(
|
||||||
|
conversationId,
|
||||||
|
user.userId,
|
||||||
|
body.content,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteDocumentVersion(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const result = await this.documentService.deleteDocumentVersion(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
10
chat/backend/src/document/document.module.ts
Normal file
10
chat/backend/src/document/document.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DocumentController } from './document.controller';
|
||||||
|
import { DocumentService } from './document.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [DocumentController],
|
||||||
|
providers: [DocumentService],
|
||||||
|
exports: [DocumentService],
|
||||||
|
})
|
||||||
|
export class DocumentModule {}
|
||||||
239
chat/backend/src/document/document.service.ts
Normal file
239
chat/backend/src/document/document.service.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||||
|
import {
|
||||||
|
type AsyncResult,
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
DatabaseError,
|
||||||
|
NotFoundError,
|
||||||
|
} from '@manacore/shared-errors';
|
||||||
|
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||||
|
import { type Database } from '../db/connection';
|
||||||
|
import {
|
||||||
|
documents,
|
||||||
|
type Document,
|
||||||
|
type NewDocument,
|
||||||
|
} from '../db/schema/documents.schema';
|
||||||
|
import { conversations } from '../db/schema/conversations.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentService {
|
||||||
|
private readonly logger = new Logger(DocumentService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private async verifyConversationOwnership(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<void> {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(conversations)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(conversations.id, conversationId),
|
||||||
|
eq(conversations.userId, userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return err(new NotFoundError('Conversation', conversationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDocument(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
content: string,
|
||||||
|
): AsyncResult<Document> {
|
||||||
|
try {
|
||||||
|
// Verify conversation ownership
|
||||||
|
const ownershipResult = await this.verifyConversationOwnership(
|
||||||
|
conversationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!ownershipResult.ok) {
|
||||||
|
return err(ownershipResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDocument: NewDocument = {
|
||||||
|
conversationId,
|
||||||
|
version: 1,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.insert(documents)
|
||||||
|
.values(newDocument)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error creating document', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to create document'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDocumentVersion(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
content: string,
|
||||||
|
): AsyncResult<Document> {
|
||||||
|
try {
|
||||||
|
// Verify conversation ownership
|
||||||
|
const ownershipResult = await this.verifyConversationOwnership(
|
||||||
|
conversationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!ownershipResult.ok) {
|
||||||
|
return err(ownershipResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest version number
|
||||||
|
const latestDoc = await this.db
|
||||||
|
.select({ version: documents.version })
|
||||||
|
.from(documents)
|
||||||
|
.where(eq(documents.conversationId, conversationId))
|
||||||
|
.orderBy(desc(documents.version))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const newVersion = (latestDoc[0]?.version || 0) + 1;
|
||||||
|
|
||||||
|
const newDocument: NewDocument = {
|
||||||
|
conversationId,
|
||||||
|
version: newVersion,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.insert(documents)
|
||||||
|
.values(newDocument)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error creating document version', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to create document version'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatestDocument(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<Document | null> {
|
||||||
|
try {
|
||||||
|
// Verify conversation ownership
|
||||||
|
const ownershipResult = await this.verifyConversationOwnership(
|
||||||
|
conversationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!ownershipResult.ok) {
|
||||||
|
return err(ownershipResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(documents)
|
||||||
|
.where(eq(documents.conversationId, conversationId))
|
||||||
|
.orderBy(desc(documents.version))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return ok(result.length > 0 ? result[0] : null);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching latest document', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch latest document'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllDocumentVersions(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<Document[]> {
|
||||||
|
try {
|
||||||
|
// Verify conversation ownership
|
||||||
|
const ownershipResult = await this.verifyConversationOwnership(
|
||||||
|
conversationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!ownershipResult.ok) {
|
||||||
|
return err(ownershipResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(documents)
|
||||||
|
.where(eq(documents.conversationId, conversationId))
|
||||||
|
.orderBy(desc(documents.version));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching document versions', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch document versions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasDocument(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<boolean> {
|
||||||
|
try {
|
||||||
|
// Verify conversation ownership
|
||||||
|
const ownershipResult = await this.verifyConversationOwnership(
|
||||||
|
conversationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!ownershipResult.ok) {
|
||||||
|
return err(ownershipResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(documents)
|
||||||
|
.where(eq(documents.conversationId, conversationId));
|
||||||
|
|
||||||
|
return ok(Number(result[0]?.count || 0) > 0);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error checking document existence', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to check document existence'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDocumentVersion(
|
||||||
|
documentId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<void> {
|
||||||
|
try {
|
||||||
|
// Get the document to verify ownership
|
||||||
|
const doc = await this.db
|
||||||
|
.select()
|
||||||
|
.from(documents)
|
||||||
|
.where(eq(documents.id, documentId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (doc.length === 0) {
|
||||||
|
return err(new NotFoundError('Document', documentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify conversation ownership
|
||||||
|
const ownershipResult = await this.verifyConversationOwnership(
|
||||||
|
doc[0].conversationId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!ownershipResult.ok) {
|
||||||
|
return err(ownershipResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db.delete(documents).where(eq(documents.id, documentId));
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error deleting document version', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to delete document version'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { AppExceptionFilter } from '@manacore/shared-errors/nestjs';
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
|
@ -13,13 +12,14 @@ async function bootstrap() {
|
||||||
'http://localhost:5173',
|
'http://localhost:5173',
|
||||||
'http://localhost:8081',
|
'http://localhost:8081',
|
||||||
'exp://localhost:8081',
|
'exp://localhost:8081',
|
||||||
|
'http://localhost:3001', // Mana Core Auth
|
||||||
],
|
],
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global exception filter for standardized error responses
|
// Global exception filter will be added later via module
|
||||||
app.useGlobalFilters(new AppExceptionFilter());
|
// app.useGlobalFilters(new AppExceptionFilter());
|
||||||
|
|
||||||
// Enable validation
|
// Enable validation
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
|
|
@ -33,7 +33,7 @@ async function bootstrap() {
|
||||||
// Set global prefix for API routes
|
// Set global prefix for API routes
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
const port = process.env.PORT || 3001;
|
const port = process.env.PORT || 3002;
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
console.log(`Chat backend running on http://localhost:${port}`);
|
console.log(`Chat backend running on http://localhost:${port}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
chat/backend/src/model/model.controller.ts
Normal file
33
chat/backend/src/model/model.controller.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||||
|
import { isOk } from '@manacore/shared-errors';
|
||||||
|
import { ModelService } from './model.service';
|
||||||
|
import { type Model } from '../db/schema/models.schema';
|
||||||
|
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@Controller('models')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class ModelController {
|
||||||
|
constructor(private readonly modelService: ModelService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getModels(): Promise<Model[]> {
|
||||||
|
const result = await this.modelService.getModels();
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
async getModel(@Param('id') id: string): Promise<Model> {
|
||||||
|
const result = await this.modelService.getModel(id);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
chat/backend/src/model/model.module.ts
Normal file
10
chat/backend/src/model/model.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ModelController } from './model.controller';
|
||||||
|
import { ModelService } from './model.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ModelController],
|
||||||
|
providers: [ModelService],
|
||||||
|
exports: [ModelService],
|
||||||
|
})
|
||||||
|
export class ModelModule {}
|
||||||
55
chat/backend/src/model/model.service.ts
Normal file
55
chat/backend/src/model/model.service.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
|
import { eq, asc } from 'drizzle-orm';
|
||||||
|
import {
|
||||||
|
type AsyncResult,
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
DatabaseError,
|
||||||
|
NotFoundError,
|
||||||
|
} from '@manacore/shared-errors';
|
||||||
|
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||||
|
import { type Database } from '../db/connection';
|
||||||
|
import { models, type Model } from '../db/schema/models.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ModelService {
|
||||||
|
private readonly logger = new Logger(ModelService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getModels(): AsyncResult<Model[]> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(models)
|
||||||
|
.where(eq(models.isActive, true))
|
||||||
|
.orderBy(asc(models.name));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching models', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch models'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getModel(id: string): AsyncResult<Model> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(models)
|
||||||
|
.where(eq(models.id, id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return err(new NotFoundError('Model', id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching model', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch model'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
chat/backend/src/space/space.controller.ts
Normal file
219
chat/backend/src/space/space.controller.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { isOk } from '@manacore/shared-errors';
|
||||||
|
import { SpaceService } from './space.service';
|
||||||
|
import { type Space, type SpaceMember } from '../db/schema/spaces.schema';
|
||||||
|
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||||
|
import {
|
||||||
|
CurrentUser,
|
||||||
|
CurrentUserData,
|
||||||
|
} from '../common/decorators/current-user.decorator';
|
||||||
|
|
||||||
|
@Controller('spaces')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class SpaceController {
|
||||||
|
constructor(private readonly spaceService: SpaceService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getUserSpaces(@CurrentUser() user: CurrentUserData): Promise<Space[]> {
|
||||||
|
const result = await this.spaceService.getUserSpaces(user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('owned')
|
||||||
|
async getOwnedSpaces(@CurrentUser() user: CurrentUserData): Promise<Space[]> {
|
||||||
|
const result = await this.spaceService.getOwnedSpaces(user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('invitations')
|
||||||
|
async getPendingInvitations(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||||
|
const result = await this.spaceService.getPendingInvitations(user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
async getSpace(@Param('id') id: string): Promise<Space> {
|
||||||
|
const result = await this.spaceService.getSpace(id);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/members')
|
||||||
|
async getSpaceMembers(
|
||||||
|
@Param('id') id: string,
|
||||||
|
): Promise<SpaceMember[]> {
|
||||||
|
const result = await this.spaceService.getSpaceMembers(id);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/role')
|
||||||
|
async getUserRoleInSpace(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ role: 'owner' | 'admin' | 'member' | 'viewer' | null }> {
|
||||||
|
const result = await this.spaceService.getUserRoleInSpace(id, user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { role: result.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async createSpace(
|
||||||
|
@Body() body: { name: string; description?: string },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Space> {
|
||||||
|
const result = await this.spaceService.createSpace(
|
||||||
|
user.userId,
|
||||||
|
body.name,
|
||||||
|
body.description,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
async updateSpace(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { name?: string; description?: string; isArchived?: boolean },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Space> {
|
||||||
|
const result = await this.spaceService.updateSpace(id, user.userId, body);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteSpace(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const result = await this.spaceService.deleteSpace(id, user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/invite')
|
||||||
|
async inviteUser(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { userId: string; role?: 'admin' | 'member' | 'viewer' },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<SpaceMember> {
|
||||||
|
const result = await this.spaceService.inviteUserToSpace(
|
||||||
|
id,
|
||||||
|
body.userId,
|
||||||
|
user.userId,
|
||||||
|
body.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/respond')
|
||||||
|
async respondToInvitation(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { status: 'accepted' | 'declined' },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<SpaceMember> {
|
||||||
|
const result = await this.spaceService.respondToInvitation(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
body.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/members/:userId')
|
||||||
|
async removeMember(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const result = await this.spaceService.removeMember(id, userId, user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/members/:userId/role')
|
||||||
|
async changeMemberRole(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('userId') userId: string,
|
||||||
|
@Body() body: { role: 'admin' | 'member' | 'viewer' },
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<SpaceMember> {
|
||||||
|
const result = await this.spaceService.changeMemberRole(
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
body.role,
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
chat/backend/src/space/space.module.ts
Normal file
10
chat/backend/src/space/space.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SpaceController } from './space.controller';
|
||||||
|
import { SpaceService } from './space.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [SpaceController],
|
||||||
|
providers: [SpaceService],
|
||||||
|
exports: [SpaceService],
|
||||||
|
})
|
||||||
|
export class SpaceModule {}
|
||||||
449
chat/backend/src/space/space.service.ts
Normal file
449
chat/backend/src/space/space.service.ts
Normal file
|
|
@ -0,0 +1,449 @@
|
||||||
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
|
import { eq, and, desc, inArray } from 'drizzle-orm';
|
||||||
|
import {
|
||||||
|
type AsyncResult,
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
DatabaseError,
|
||||||
|
NotFoundError,
|
||||||
|
} from '@manacore/shared-errors';
|
||||||
|
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||||
|
import { type Database } from '../db/connection';
|
||||||
|
import {
|
||||||
|
spaces,
|
||||||
|
spaceMembers,
|
||||||
|
type Space,
|
||||||
|
type NewSpace,
|
||||||
|
type SpaceMember,
|
||||||
|
type NewSpaceMember,
|
||||||
|
} from '../db/schema/spaces.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SpaceService {
|
||||||
|
private readonly logger = new Logger(SpaceService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getUserSpaces(userId: string): AsyncResult<Space[]> {
|
||||||
|
try {
|
||||||
|
// Get all space IDs where user is an accepted member
|
||||||
|
const memberData = await this.db
|
||||||
|
.select({ spaceId: spaceMembers.spaceId })
|
||||||
|
.from(spaceMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
eq(spaceMembers.invitationStatus, 'accepted'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (memberData.length === 0) {
|
||||||
|
return ok([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceIds = memberData.map((m) => m.spaceId);
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaces)
|
||||||
|
.where(and(inArray(spaces.id, spaceIds), eq(spaces.isArchived, false)))
|
||||||
|
.orderBy(desc(spaces.createdAt));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching user spaces', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch user spaces'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOwnedSpaces(userId: string): AsyncResult<Space[]> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaces)
|
||||||
|
.where(and(eq(spaces.ownerId, userId), eq(spaces.isArchived, false)))
|
||||||
|
.orderBy(desc(spaces.createdAt));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching owned spaces', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch owned spaces'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpace(id: string): AsyncResult<Space> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaces)
|
||||||
|
.where(eq(spaces.id, id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return err(new NotFoundError('Space', id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching space', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch space'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSpace(
|
||||||
|
userId: string,
|
||||||
|
name: string,
|
||||||
|
description?: string,
|
||||||
|
): AsyncResult<Space> {
|
||||||
|
try {
|
||||||
|
const newSpace: NewSpace = {
|
||||||
|
ownerId: userId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
isArchived: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.insert(spaces)
|
||||||
|
.values(newSpace)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Add owner as an accepted member
|
||||||
|
const memberData: NewSpaceMember = {
|
||||||
|
spaceId: result[0].id,
|
||||||
|
userId,
|
||||||
|
role: 'owner',
|
||||||
|
invitationStatus: 'accepted',
|
||||||
|
joinedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.db.insert(spaceMembers).values(memberData);
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error creating space', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to create space'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSpace(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
data: { name?: string; description?: string; isArchived?: boolean },
|
||||||
|
): AsyncResult<Space> {
|
||||||
|
try {
|
||||||
|
// Verify ownership
|
||||||
|
const spaceResult = await this.getSpace(id);
|
||||||
|
if (!spaceResult.ok) {
|
||||||
|
return err(spaceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spaceResult.value.ownerId !== userId) {
|
||||||
|
return err(new NotFoundError('Space', id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.update(spaces)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(spaces.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error updating space', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to update space'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSpace(id: string, userId: string): AsyncResult<void> {
|
||||||
|
try {
|
||||||
|
// Verify ownership
|
||||||
|
const spaceResult = await this.getSpace(id);
|
||||||
|
if (!spaceResult.ok) {
|
||||||
|
return err(spaceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spaceResult.value.ownerId !== userId) {
|
||||||
|
return err(new NotFoundError('Space', id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Members will be cascade deleted
|
||||||
|
await this.db.delete(spaces).where(eq(spaces.id, id));
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error deleting space', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to delete space'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpaceMembers(spaceId: string): AsyncResult<SpaceMember[]> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaceMembers)
|
||||||
|
.where(eq(spaceMembers.spaceId, spaceId))
|
||||||
|
.orderBy(spaceMembers.role, desc(spaceMembers.joinedAt));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching space members', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch space members'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async inviteUserToSpace(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
invitedByUserId: string,
|
||||||
|
role: 'admin' | 'member' | 'viewer' = 'member',
|
||||||
|
): AsyncResult<SpaceMember> {
|
||||||
|
try {
|
||||||
|
// Check if user is already a member
|
||||||
|
const existingMember = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaceMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.spaceId, spaceId),
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingMember.length > 0) {
|
||||||
|
if (existingMember[0].invitationStatus === 'accepted') {
|
||||||
|
return ok(existingMember[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing invitation
|
||||||
|
const result = await this.db
|
||||||
|
.update(spaceMembers)
|
||||||
|
.set({
|
||||||
|
role,
|
||||||
|
invitationStatus: 'pending',
|
||||||
|
invitedBy: invitedByUserId,
|
||||||
|
invitedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(spaceMembers.id, existingMember[0].id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new invitation
|
||||||
|
const memberData: NewSpaceMember = {
|
||||||
|
spaceId,
|
||||||
|
userId,
|
||||||
|
role,
|
||||||
|
invitationStatus: 'pending',
|
||||||
|
invitedBy: invitedByUserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.insert(spaceMembers)
|
||||||
|
.values(memberData)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error inviting user to space', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to invite user to space'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async respondToInvitation(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
status: 'accepted' | 'declined',
|
||||||
|
): AsyncResult<SpaceMember> {
|
||||||
|
try {
|
||||||
|
const updates: Partial<SpaceMember> = {
|
||||||
|
invitationStatus: status,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === 'accepted') {
|
||||||
|
updates.joinedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.update(spaceMembers)
|
||||||
|
.set(updates)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.spaceId, spaceId),
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return err(new NotFoundError('SpaceMember', `${spaceId}:${userId}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error responding to invitation', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to respond to invitation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMember(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
requestingUserId: string,
|
||||||
|
): AsyncResult<void> {
|
||||||
|
try {
|
||||||
|
// Verify the requesting user is the owner or an admin
|
||||||
|
const spaceResult = await this.getSpace(spaceId);
|
||||||
|
if (!spaceResult.ok) {
|
||||||
|
return err(spaceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestingMember = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaceMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.spaceId, spaceId),
|
||||||
|
eq(spaceMembers.userId, requestingUserId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const isOwner = spaceResult.value.ownerId === requestingUserId;
|
||||||
|
const isAdmin =
|
||||||
|
requestingMember.length > 0 && requestingMember[0].role === 'admin';
|
||||||
|
|
||||||
|
if (!isOwner && !isAdmin) {
|
||||||
|
return err(new NotFoundError('SpaceMember', `${spaceId}:${userId}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db
|
||||||
|
.delete(spaceMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.spaceId, spaceId),
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error removing member', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to remove member'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeMemberRole(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
newRole: 'admin' | 'member' | 'viewer',
|
||||||
|
requestingUserId: string,
|
||||||
|
): AsyncResult<SpaceMember> {
|
||||||
|
try {
|
||||||
|
// Verify the requesting user is the owner
|
||||||
|
const spaceResult = await this.getSpace(spaceId);
|
||||||
|
if (!spaceResult.ok) {
|
||||||
|
return err(spaceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spaceResult.value.ownerId !== requestingUserId) {
|
||||||
|
return err(new NotFoundError('SpaceMember', `${spaceId}:${userId}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.update(spaceMembers)
|
||||||
|
.set({ role: newRole, updatedAt: new Date() })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.spaceId, spaceId),
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return err(new NotFoundError('SpaceMember', `${spaceId}:${userId}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error changing member role', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to change member role'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserRoleInSpace(
|
||||||
|
spaceId: string,
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||||
|
try {
|
||||||
|
// Check if owner
|
||||||
|
const spaceResult = await this.getSpace(spaceId);
|
||||||
|
if (!spaceResult.ok) {
|
||||||
|
return err(spaceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spaceResult.value.ownerId === userId) {
|
||||||
|
return ok('owner');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check membership
|
||||||
|
const memberResult = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaceMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.spaceId, spaceId),
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
eq(spaceMembers.invitationStatus, 'accepted'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (memberResult.length === 0) {
|
||||||
|
return ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(memberResult[0].role as 'admin' | 'member' | 'viewer');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error getting user role in space', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to get user role in space'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPendingInvitations(
|
||||||
|
userId: string,
|
||||||
|
): AsyncResult<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||||
|
try {
|
||||||
|
const invitations = await this.db
|
||||||
|
.select()
|
||||||
|
.from(spaceMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(spaceMembers.userId, userId),
|
||||||
|
eq(spaceMembers.invitationStatus, 'pending'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const results: Array<{ invitation: SpaceMember; space: Space }> = [];
|
||||||
|
|
||||||
|
for (const invitation of invitations) {
|
||||||
|
const spaceResult = await this.getSpace(invitation.spaceId);
|
||||||
|
if (spaceResult.ok) {
|
||||||
|
results.push({ invitation, space: spaceResult.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(results);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching pending invitations', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch pending invitations'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
chat/backend/src/template/template.controller.ts
Normal file
141
chat/backend/src/template/template.controller.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { isOk } from '@manacore/shared-errors';
|
||||||
|
import { TemplateService } from './template.service';
|
||||||
|
import { type Template } from '../db/schema/templates.schema';
|
||||||
|
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||||
|
import {
|
||||||
|
CurrentUser,
|
||||||
|
CurrentUserData,
|
||||||
|
} from '../common/decorators/current-user.decorator';
|
||||||
|
|
||||||
|
@Controller('templates')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class TemplateController {
|
||||||
|
constructor(private readonly templateService: TemplateService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getTemplates(@CurrentUser() user: CurrentUserData): Promise<Template[]> {
|
||||||
|
const result = await this.templateService.getTemplates(user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('default')
|
||||||
|
async getDefaultTemplate(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Template | null> {
|
||||||
|
const result = await this.templateService.getDefaultTemplate(user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
async getTemplate(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Template> {
|
||||||
|
const result = await this.templateService.getTemplate(id, user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async createTemplate(
|
||||||
|
@Body()
|
||||||
|
body: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion?: string;
|
||||||
|
modelId?: string;
|
||||||
|
color?: string;
|
||||||
|
documentMode?: boolean;
|
||||||
|
},
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Template> {
|
||||||
|
const result = await this.templateService.createTemplate(user.userId, body);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
async updateTemplate(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body()
|
||||||
|
body: Partial<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion: string;
|
||||||
|
modelId: string;
|
||||||
|
color: string;
|
||||||
|
documentMode: boolean;
|
||||||
|
}>,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Template> {
|
||||||
|
const result = await this.templateService.updateTemplate(
|
||||||
|
id,
|
||||||
|
user.userId,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/default')
|
||||||
|
async setDefaultTemplate(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<Template> {
|
||||||
|
const result = await this.templateService.setDefaultTemplate(id, user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteTemplate(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const result = await this.templateService.deleteTemplate(id, user.userId);
|
||||||
|
|
||||||
|
if (!isOk(result)) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
10
chat/backend/src/template/template.module.ts
Normal file
10
chat/backend/src/template/template.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TemplateController } from './template.controller';
|
||||||
|
import { TemplateService } from './template.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [TemplateController],
|
||||||
|
providers: [TemplateService],
|
||||||
|
exports: [TemplateService],
|
||||||
|
})
|
||||||
|
export class TemplateModule {}
|
||||||
191
chat/backend/src/template/template.service.ts
Normal file
191
chat/backend/src/template/template.service.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
|
import { eq, and, asc } from 'drizzle-orm';
|
||||||
|
import {
|
||||||
|
type AsyncResult,
|
||||||
|
ok,
|
||||||
|
err,
|
||||||
|
DatabaseError,
|
||||||
|
NotFoundError,
|
||||||
|
} from '@manacore/shared-errors';
|
||||||
|
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||||
|
import { type Database } from '../db/connection';
|
||||||
|
import {
|
||||||
|
templates,
|
||||||
|
type Template,
|
||||||
|
type NewTemplate,
|
||||||
|
} from '../db/schema/templates.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TemplateService {
|
||||||
|
private readonly logger = new Logger(TemplateService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getTemplates(userId: string): AsyncResult<Template[]> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(templates)
|
||||||
|
.where(eq(templates.userId, userId))
|
||||||
|
.orderBy(asc(templates.name));
|
||||||
|
|
||||||
|
return ok(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching templates', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch templates'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTemplate(id: string, userId: string): AsyncResult<Template> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(templates)
|
||||||
|
.where(and(eq(templates.id, id), eq(templates.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return err(new NotFoundError('Template', id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching template', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultTemplate(userId: string): AsyncResult<Template | null> {
|
||||||
|
try {
|
||||||
|
const result = await this.db
|
||||||
|
.select()
|
||||||
|
.from(templates)
|
||||||
|
.where(
|
||||||
|
and(eq(templates.userId, userId), eq(templates.isDefault, true)),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return ok(result.length > 0 ? result[0] : null);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching default template', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to fetch default template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTemplate(
|
||||||
|
userId: string,
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion?: string;
|
||||||
|
modelId?: string;
|
||||||
|
color?: string;
|
||||||
|
documentMode?: boolean;
|
||||||
|
},
|
||||||
|
): AsyncResult<Template> {
|
||||||
|
try {
|
||||||
|
const newTemplate: NewTemplate = {
|
||||||
|
userId,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
systemPrompt: data.systemPrompt,
|
||||||
|
initialQuestion: data.initialQuestion,
|
||||||
|
modelId: data.modelId,
|
||||||
|
color: data.color || '#3b82f6',
|
||||||
|
documentMode: data.documentMode || false,
|
||||||
|
isDefault: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.insert(templates)
|
||||||
|
.values(newTemplate)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error creating template', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to create template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTemplate(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
data: Partial<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
initialQuestion: string;
|
||||||
|
modelId: string;
|
||||||
|
color: string;
|
||||||
|
documentMode: boolean;
|
||||||
|
}>,
|
||||||
|
): AsyncResult<Template> {
|
||||||
|
try {
|
||||||
|
// First verify the template belongs to the user
|
||||||
|
const templateResult = await this.getTemplate(id, userId);
|
||||||
|
if (!templateResult.ok) {
|
||||||
|
return err(templateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.db
|
||||||
|
.update(templates)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(templates.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error updating template', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to update template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultTemplate(id: string, userId: string): AsyncResult<Template> {
|
||||||
|
try {
|
||||||
|
// First verify the template belongs to the user
|
||||||
|
const templateResult = await this.getTemplate(id, userId);
|
||||||
|
if (!templateResult.ok) {
|
||||||
|
return err(templateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all default flags for this user
|
||||||
|
await this.db
|
||||||
|
.update(templates)
|
||||||
|
.set({ isDefault: false, updatedAt: new Date() })
|
||||||
|
.where(eq(templates.userId, userId));
|
||||||
|
|
||||||
|
// Set the new default
|
||||||
|
const result = await this.db
|
||||||
|
.update(templates)
|
||||||
|
.set({ isDefault: true, updatedAt: new Date() })
|
||||||
|
.where(eq(templates.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return ok(result[0]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error setting default template', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to set default template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTemplate(id: string, userId: string): AsyncResult<void> {
|
||||||
|
try {
|
||||||
|
// First verify the template belongs to the user
|
||||||
|
const templateResult = await this.getTemplate(id, userId);
|
||||||
|
if (!templateResult.ok) {
|
||||||
|
return err(templateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db.delete(templates).where(eq(templates.id, id));
|
||||||
|
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error deleting template', error);
|
||||||
|
return err(DatabaseError.queryFailed('Failed to delete template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
chat/backend/tsconfig.build.json
Normal file
4
chat/backend/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
"rootDir": "./src",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
|
|
@ -17,5 +19,7 @@
|
||||||
"strictBindCallApply": true,
|
"strictBindCallApply": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
}
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
116
docker-compose.dev.yml
Normal file
116
docker-compose.dev.yml
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# Development Docker Compose - Unified local development setup
|
||||||
|
# Start infrastructure: pnpm docker:up
|
||||||
|
# Start all services: pnpm docker:up:all
|
||||||
|
# Stop: pnpm docker:down
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Shared PostgreSQL Database (multiple databases)
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: manacore-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: manacore
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-manacore}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpassword}
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
- ./docker/init-db:/docker-entrypoint-initdb.d:ro
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- manacore-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-manacore}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Redis cache
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: manacore-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD:-devpassword} --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- manacore-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-devpassword}", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Mana Core Auth Service
|
||||||
|
mana-core-auth:
|
||||||
|
profiles: ["auth", "all"]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./mana-core-auth/Dockerfile
|
||||||
|
container_name: manacore-auth
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
PORT: 3001
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-manacore}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/manacore
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-devpassword}
|
||||||
|
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
|
||||||
|
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRY: ${JWT_ACCESS_TOKEN_EXPIRY:-15m}
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRY: ${JWT_REFRESH_TOKEN_EXPIRY:-7d}
|
||||||
|
JWT_ISSUER: ${JWT_ISSUER:-manacore}
|
||||||
|
JWT_AUDIENCE: ${JWT_AUDIENCE:-manacore}
|
||||||
|
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173,http://localhost:8081}
|
||||||
|
CREDITS_SIGNUP_BONUS: ${CREDITS_SIGNUP_BONUS:-150}
|
||||||
|
CREDITS_DAILY_FREE: ${CREDITS_DAILY_FREE:-5}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
networks:
|
||||||
|
- manacore-network
|
||||||
|
|
||||||
|
# Chat Backend Service
|
||||||
|
chat-backend:
|
||||||
|
profiles: ["chat", "all"]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: chat/backend/Dockerfile
|
||||||
|
container_name: chat-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
PORT: 3002
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-manacore}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/chat
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: ${POSTGRES_USER:-manacore}
|
||||||
|
DB_PASSWORD: ${POSTGRES_PASSWORD:-devpassword}
|
||||||
|
DB_NAME: chat
|
||||||
|
AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT}
|
||||||
|
AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY}
|
||||||
|
AZURE_OPENAI_API_VERSION: ${AZURE_OPENAI_API_VERSION:-2024-12-01-preview}
|
||||||
|
MANA_CORE_AUTH_URL: http://mana-core-auth:3001
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "3002:3002"
|
||||||
|
networks:
|
||||||
|
- manacore-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
manacore-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
redis-data:
|
||||||
9
docker/init-db/01-create-databases.sql
Normal file
9
docker/init-db/01-create-databases.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- Create databases for all services
|
||||||
|
-- This script runs on first container initialization
|
||||||
|
|
||||||
|
-- Create chat database
|
||||||
|
CREATE DATABASE chat;
|
||||||
|
|
||||||
|
-- Grant all privileges to the default user
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE chat TO manacore;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE manacore TO manacore;
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
"@google-cloud/aiplatform": "^3.34.0",
|
"@google-cloud/aiplatform": "^3.34.0",
|
||||||
"@google-cloud/storage": "^7.15.0",
|
"@google-cloud/storage": "^7.15.0",
|
||||||
"@google/genai": "^1.14.0",
|
"@google/genai": "^1.14.0",
|
||||||
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
|
|
||||||
"@nestjs/axios": "^4.0.0",
|
"@nestjs/axios": "^4.0.0",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^4.0.0",
|
"@nestjs/config": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@manacore/shared-errors": "workspace:*",
|
"@manacore/shared-errors": "workspace:*",
|
||||||
"@google/genai": "^1.14.0",
|
"@google/genai": "^1.14.0",
|
||||||
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
|
|
||||||
"@manacore/manadeck-database": "workspace:*",
|
"@manacore/manadeck-database": "workspace:*",
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
|
|
|
||||||
13
package.json
13
package.json
|
|
@ -67,7 +67,18 @@
|
||||||
"dev:news:api": "pnpm --filter @news/api start:dev",
|
"dev:news:api": "pnpm --filter @news/api start:dev",
|
||||||
"dev:news:app": "turbo run dev --filter=@news/web --filter=@news/api",
|
"dev:news:app": "turbo run dev --filter=@news/web --filter=@news/api",
|
||||||
"news:db:push": "pnpm --filter @manacore/news-database db:push",
|
"news:db:push": "pnpm --filter @manacore/news-database db:push",
|
||||||
"news:db:studio": "pnpm --filter @manacore/news-database db:studio"
|
"news:db:studio": "pnpm --filter @manacore/news-database db:studio",
|
||||||
|
|
||||||
|
"docker:up": "docker compose -f docker-compose.dev.yml up -d postgres redis",
|
||||||
|
"docker:up:auth": "docker compose -f docker-compose.dev.yml --profile auth up -d",
|
||||||
|
"docker:up:chat": "docker compose -f docker-compose.dev.yml --profile chat up -d",
|
||||||
|
"docker:up:all": "docker compose -f docker-compose.dev.yml --profile all up -d",
|
||||||
|
"docker:down": "docker compose -f docker-compose.dev.yml --profile all down",
|
||||||
|
"docker:logs": "docker compose -f docker-compose.dev.yml logs -f",
|
||||||
|
"docker:logs:auth": "docker compose -f docker-compose.dev.yml logs -f mana-core-auth",
|
||||||
|
"docker:logs:chat": "docker compose -f docker-compose.dev.yml logs -f chat-backend",
|
||||||
|
"docker:ps": "docker compose -f docker-compose.dev.yml ps -a",
|
||||||
|
"docker:clean": "docker compose -f docker-compose.dev.yml --profile all down -v"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"verbatimModuleSyntax": true,
|
"declaration": true,
|
||||||
"noEmit": true
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,20 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Go-like error handling system for Manacore backends",
|
"description": "Go-like error handling system for Manacore backends",
|
||||||
"main": "./src/index.ts",
|
"main": "./dist/index.js",
|
||||||
"types": "./src/index.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": {
|
||||||
"./nestjs": "./src/nestjs/index.ts"
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./nestjs": {
|
||||||
|
"types": "./dist/nestjs/index.d.ts",
|
||||||
|
"default": "./dist/nestjs/index.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
18
packages/shared-errors/tsconfig.build.json
Normal file
18
packages/shared-errors/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
3297
pnpm-lock.yaml
generated
3297
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,6 @@
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
|
|
||||||
"@manacore/uload-database": "workspace:*",
|
"@manacore/uload-database": "workspace:*",
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue