mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:01:08 +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 = {
|
||||
id: string; // UUID
|
||||
user_id: string; // UUID des Benutzers (auth.uid)
|
||||
model_id: string; // UUID
|
||||
template_id?: string; // UUID, optional
|
||||
id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
template_id?: string;
|
||||
space_id?: string;
|
||||
conversation_mode: 'free' | 'guided' | 'template';
|
||||
document_mode: boolean; // Gibt an, ob der Dokumentmodus aktiviert ist
|
||||
title?: string; // Titel der Konversation
|
||||
is_archived: boolean; // Gibt an, ob die Konversation archiviert wurde
|
||||
document_mode: boolean;
|
||||
title?: string;
|
||||
is_archived: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
|
@ -37,6 +50,34 @@ export type TokenUsageType = {
|
|||
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
|
||||
*/
|
||||
|
|
@ -49,28 +90,22 @@ export async function createConversation(
|
|||
spaceId?: string
|
||||
): Promise<string | null> {
|
||||
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 { data, error } = await supabase
|
||||
.from('conversations')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
model_id: modelId,
|
||||
template_id: templateId,
|
||||
conversation_mode: mode,
|
||||
document_mode: documentMode,
|
||||
space_id: spaceId,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
const conversation = await conversationApi.createConversation({
|
||||
modelId,
|
||||
conversationMode: mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Erstellen der Konversation:', error);
|
||||
if (!conversation) {
|
||||
console.error('Fehler beim Erstellen der Konversation');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.id;
|
||||
return conversation.id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Konversation:', error);
|
||||
return null;
|
||||
|
|
@ -86,35 +121,23 @@ export async function addMessage(
|
|||
messageText: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// Führe eine Prüfung und Validierung des Senders durch
|
||||
// Validate 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)) {
|
||||
console.error('Ungültiger Sender-Wert:', sender);
|
||||
validSender = 'user'; // Fallback auf 'user'
|
||||
validSender = 'user';
|
||||
}
|
||||
|
||||
console.log('Füge Nachricht hinzu mit Sender:', validSender);
|
||||
|
||||
// Füge eine neue Nachricht in die Messages-Tabelle ein
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.insert({
|
||||
conversation_id: conversationId,
|
||||
sender: validSender,
|
||||
message_text: messageText,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
const message = await conversationApi.addMessage(conversationId, validSender, messageText);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
||||
if (!message) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.id;
|
||||
return message.id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
||||
return null;
|
||||
|
|
@ -126,18 +149,8 @@ export async function addMessage(
|
|||
*/
|
||||
export async function getMessages(conversationId: string): Promise<Message[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.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[];
|
||||
const messages = await conversationApi.getMessages(conversationId);
|
||||
return messages.map(toLocalMessage);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Nachrichten:', error);
|
||||
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
|
||||
* @param userQuestion Die erste Frage des Benutzers
|
||||
* @returns Generierter Titel
|
||||
*/
|
||||
export async function generateConversationTitle(userQuestion: string): Promise<string> {
|
||||
try {
|
||||
console.log('Generiere Titel für Konversation basierend auf:',
|
||||
userQuestion.substring(0, 50) + (userQuestion.length > 50 ? '...' : ''));
|
||||
console.log(
|
||||
'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}"`;
|
||||
|
||||
// Manuell 4o-mini-Modell festlegen
|
||||
const chatMessages: ChatMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'MODEL:gpt-4o-mini-se'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: titlePrompt
|
||||
}
|
||||
];
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: [{ role: 'user', content: titlePrompt }],
|
||||
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
||||
temperature: 0.3,
|
||||
maxTokens: 50,
|
||||
});
|
||||
|
||||
// Sende die Anfrage mit niedrigerer Temperatur für zuverlässigere Ergebnisse
|
||||
const titleResponse = await sendChatRequest(chatMessages, 0.3, 50);
|
||||
if (!response) {
|
||||
return 'Neue Konversation';
|
||||
}
|
||||
|
||||
// Extrahiere den Text aus der Antwort
|
||||
const responseText = typeof titleResponse === 'string'
|
||||
? titleResponse
|
||||
: titleResponse.content;
|
||||
// Clean up title
|
||||
let cleanTitle = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
|
||||
// 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) {
|
||||
cleanTitle = cleanTitle.substring(0, 97) + '...';
|
||||
}
|
||||
|
|
@ -191,7 +194,6 @@ export async function generateConversationTitle(userQuestion: string): Promise<s
|
|||
return cleanTitle;
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Titelgenerierung:', error);
|
||||
// Fallback-Titel bei Fehler
|
||||
return 'Neue Konversation';
|
||||
}
|
||||
}
|
||||
|
|
@ -199,15 +201,15 @@ export async function generateConversationTitle(userQuestion: string): Promise<s
|
|||
/**
|
||||
* 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 {
|
||||
const { error } = await supabase
|
||||
.from('conversations')
|
||||
.update({ title })
|
||||
.eq('id', conversationId);
|
||||
const success = await conversationApi.updateTitle(conversationId, title);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Aktualisieren des Konversationstitels:', error);
|
||||
if (!success) {
|
||||
console.error('Fehler beim Aktualisieren des Konversationstitels');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -220,23 +222,17 @@ export async function updateConversationTitle(conversationId: string, title: str
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.select('system_prompt')
|
||||
.eq('id', templateId)
|
||||
.single();
|
||||
const template = await templateApi.getTemplate(templateId);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
if (!template) {
|
||||
console.error('Fehler beim Laden der Vorlage');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.system_prompt;
|
||||
return template.systemPrompt;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
return null;
|
||||
|
|
@ -252,70 +248,52 @@ export async function sendMessageAndGetResponse(
|
|||
modelId: string,
|
||||
templateId?: string,
|
||||
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 {
|
||||
console.log('Starte sendMessageAndGetResponse mit:', {
|
||||
conversationId,
|
||||
userMessage: userMessage.substring(0, 50) + (userMessage.length > 50 ? '...' : ''),
|
||||
modelId
|
||||
modelId,
|
||||
});
|
||||
|
||||
// Lade das Modell aus der Datenbank oder verwende Fallback
|
||||
const { data: modelData, error: modelError } = await supabase
|
||||
.from('models')
|
||||
.select('*')
|
||||
.eq('id', modelId)
|
||||
.single();
|
||||
// Load the model from API
|
||||
const modelData = await modelApi.getModel(modelId);
|
||||
|
||||
if (modelError) {
|
||||
console.error('Fehler beim Laden des Modells:', modelError);
|
||||
if (!modelData) {
|
||||
console.log('Verwende Standard-Parameter, da Modell nicht geladen werden konnte');
|
||||
} else {
|
||||
console.log('Modell geladen:', {
|
||||
id: modelData.id,
|
||||
name: modelData.name,
|
||||
parameters: modelData.parameters,
|
||||
deployment: modelData.parameters?.deployment
|
||||
deployment: modelData.parameters?.deployment,
|
||||
});
|
||||
}
|
||||
|
||||
// Variable für die Benutzer-Nachricht-ID
|
||||
let userMessageId: string | null = null;
|
||||
// Save the user message
|
||||
const userMessageId = await addMessage(conversationId, 'user', userMessage);
|
||||
console.log('Benutzernachricht gespeichert mit ID:', userMessageId);
|
||||
|
||||
// Überprüfe, ob die Nachricht bereits in der Datenbank existiert
|
||||
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
|
||||
// Load all messages for context
|
||||
const messages = await getMessages(conversationId);
|
||||
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[] = [];
|
||||
|
||||
// Lade den System-Prompt aus der Vorlage, falls template_id vorhanden
|
||||
// Load system prompt from template if available
|
||||
let systemPrompt: string | null = null;
|
||||
if (templateId) {
|
||||
systemPrompt = await getSystemPromptFromTemplate(templateId);
|
||||
if (systemPrompt) {
|
||||
console.log('System-Prompt aus Vorlage geladen');
|
||||
|
||||
// Wenn Dokumentmodus aktiv ist, füge spezielle Anweisungen hinzu
|
||||
if (documentMode) {
|
||||
const documentModePrompt = `
|
||||
${systemPrompt}
|
||||
|
|
@ -333,23 +311,13 @@ CHAT: Hier antwortest du auf die Frage oder das Feedback des Nutzers.
|
|||
DOKUMENT:
|
||||
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');
|
||||
} else {
|
||||
// Standard-Prompt ohne Dokumentmodus
|
||||
chatMessages.push({
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
});
|
||||
chatMessages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
}
|
||||
} else if (documentMode) {
|
||||
// Wenn kein Template, aber Dokumentmodus aktiv ist
|
||||
const documentModePrompt = `
|
||||
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:
|
||||
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
||||
`;
|
||||
chatMessages.push({
|
||||
role: 'system',
|
||||
content: documentModePrompt
|
||||
});
|
||||
|
||||
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||
console.log('Standard-Dokumentmodus-Prompt hinzugefügt');
|
||||
}
|
||||
|
||||
// Füge eine System-Nachricht mit der Modell-ID hinzu, falls ein Modell geladen wurde
|
||||
if (modelData && modelData.parameters && modelData.parameters.deployment) {
|
||||
console.log(`Nutze deployment '${modelData.parameters.deployment}' für Modell ${modelData.name}`);
|
||||
// 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',
|
||||
content: msg.message_text
|
||||
})));
|
||||
// Add all conversation messages
|
||||
chatMessages.push(
|
||||
...messages.map((msg) => ({
|
||||
role: msg.sender === 'user' ? 'user' : msg.sender === 'assistant' ? 'assistant' : 'system',
|
||||
content: msg.message_text,
|
||||
})) as ChatMessage[]
|
||||
);
|
||||
|
||||
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:', {
|
||||
temperature: modelData?.parameters?.temperature || 0.7,
|
||||
maxTokens: modelData?.parameters?.max_tokens || 800
|
||||
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||
});
|
||||
|
||||
const result = await sendChatRequest(
|
||||
chatMessages,
|
||||
modelData?.parameters?.temperature || 0.7,
|
||||
modelData?.parameters?.max_tokens || 800
|
||||
);
|
||||
const result = await chatApi.createCompletion({
|
||||
messages: chatMessages,
|
||||
modelId,
|
||||
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 tokenUsage;
|
||||
let tokenUsage: TokenUsage | undefined;
|
||||
|
||||
if (typeof result === 'string') {
|
||||
// Falls nur ein String zurückgegeben wurde (Fehlerfall)
|
||||
assistantResponse = result;
|
||||
console.log('Einfache String-Antwort vom LLM-Modell erhalten (kein Tokennutzungs-Tracking):', {
|
||||
length: assistantResponse.length,
|
||||
preview: assistantResponse.substring(0, 50) + (assistantResponse.length > 50 ? '...' : '')
|
||||
});
|
||||
if (!result) {
|
||||
assistantResponse =
|
||||
'Es konnte keine Antwort generiert werden. Bitte stelle sicher, dass das Backend läuft.';
|
||||
} else {
|
||||
// Bei vollständigem Ergebnis mit Token-Nutzung
|
||||
assistantResponse = result.content;
|
||||
tokenUsage = result.usage;
|
||||
|
||||
console.log('Antwort vom LLM-Modell erhalten:', {
|
||||
length: assistantResponse.length,
|
||||
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 chatResponse = assistantResponse;
|
||||
|
||||
if (documentMode) {
|
||||
// Nach dem Format "CHAT: ... DOKUMENT: ```markdown ... ```" suchen
|
||||
console.log("Analysiere LLM-Antwort für Dokumentextraktion:", assistantResponse.substring(0, 200) + "...");
|
||||
console.log(
|
||||
'Analysiere LLM-Antwort für Dokumentextraktion:',
|
||||
assistantResponse.substring(0, 200) + '...'
|
||||
);
|
||||
|
||||
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]) {
|
||||
chatResponse = chatMatch[1].trim();
|
||||
|
|
@ -468,67 +400,52 @@ Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-M
|
|||
|
||||
if (documentMatch && documentMatch[2]) {
|
||||
documentContent = documentMatch[2].trim();
|
||||
console.log('Dokument-Inhalt extrahiert:', (documentContent?.substring(0, 50) || '') + '...');
|
||||
} else if (documentMatch) {
|
||||
console.log('Dokument-Match gefunden, aber kein Inhalt in Gruppe 2');
|
||||
console.log('Dokument-Match-Gruppen:', documentMatch.length);
|
||||
for (let i = 0; i < documentMatch.length; i++) {
|
||||
console.log(`Gruppe ${i}:`, documentMatch[i]?.substring(0, 30));
|
||||
}
|
||||
console.log(
|
||||
'Dokument-Inhalt extrahiert:',
|
||||
(documentContent?.substring(0, 50) || '') + '...'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Speichere die Antwort des Assistenten
|
||||
// Save assistant message
|
||||
const assistantMessageId = await addMessage(conversationId, 'assistant', chatResponse);
|
||||
console.log('Assistentenantwort gespeichert mit ID:', assistantMessageId);
|
||||
|
||||
// Token-Nutzung loggen, falls verfügbar
|
||||
if (tokenUsage && assistantMessageId && userMessageId && modelData) {
|
||||
// Log token usage if available
|
||||
if (tokenUsage && assistantMessageId && userMessageId) {
|
||||
try {
|
||||
// Lade die Konversation, um die user_id zu erhalten
|
||||
const { data: conversationData, error: convError } = await supabase
|
||||
.from('conversations')
|
||||
.select('user_id')
|
||||
.eq('id', conversationId)
|
||||
.single();
|
||||
const estimatedCost = calculateTokenCost(
|
||||
tokenUsage.prompt_tokens,
|
||||
tokenUsage.completion_tokens,
|
||||
modelData?.costSettings
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
console.log('Token-Nutzung erfolgreich geloggt');
|
||||
}
|
||||
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');
|
||||
} catch (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
|
||||
// Wenn ja, generiere einen Titel und aktualisiere die Konversation
|
||||
const { count } = await supabase
|
||||
.from('messages')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('conversation_id', conversationId);
|
||||
// Generate title for new conversations
|
||||
const allMessages = await getMessages(conversationId);
|
||||
let title: string | undefined;
|
||||
|
||||
let title;
|
||||
// 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
|
||||
if (allMessages.length <= 2) {
|
||||
title = await generateConversationTitle(userMessage);
|
||||
|
||||
// Aktualisiere den Titel in der Datenbank
|
||||
const success = await updateConversationTitle(conversationId, title);
|
||||
console.log('Konversationstitel aktualisiert:', success ? 'erfolgreich' : 'fehlgeschlagen');
|
||||
if (title) {
|
||||
const success = await updateConversationTitle(conversationId, title);
|
||||
console.log('Konversationstitel aktualisiert:', success ? 'erfolgreich' : 'fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -536,53 +453,47 @@ Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-M
|
|||
assistantMessageId,
|
||||
assistantResponse: chatResponse,
|
||||
title,
|
||||
documentContent
|
||||
documentContent,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden der Nachricht:', error);
|
||||
|
||||
// Detaillierte Fehlerinformationen ausgeben
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerdetails:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
userMessageId: 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
|
||||
* Optional: Mit spaceId werden nur Konversationen aus diesem Space geladen
|
||||
*/
|
||||
export async function getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||
try {
|
||||
let query = supabase
|
||||
.from('conversations')
|
||||
.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[];
|
||||
const conversations = await conversationApi.getConversations(spaceId);
|
||||
return conversations.map(toLocalConversation);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konversationen:', error);
|
||||
return [];
|
||||
|
|
@ -594,19 +505,8 @@ export async function getConversations(userId: string, spaceId?: string): Promis
|
|||
*/
|
||||
export async function getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversations')
|
||||
.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[];
|
||||
const conversations = await conversationApi.getArchivedConversations();
|
||||
return conversations.map(toLocalConversation);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der archivierten Konversationen:', error);
|
||||
return [];
|
||||
|
|
@ -618,17 +518,7 @@ export async function getArchivedConversations(userId: string): Promise<Conversa
|
|||
*/
|
||||
export async function archiveConversation(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('conversations')
|
||||
.update({ is_archived: true })
|
||||
.eq('id', conversationId);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Archivieren der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return await conversationApi.archiveConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Archivieren der Konversation:', error);
|
||||
return false;
|
||||
|
|
@ -640,17 +530,7 @@ export async function archiveConversation(conversationId: string): Promise<boole
|
|||
*/
|
||||
export async function unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('conversations')
|
||||
.update({ is_archived: false })
|
||||
.eq('id', conversationId);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return await conversationApi.unarchiveConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
||||
return false;
|
||||
|
|
@ -662,29 +542,7 @@ export async function unarchiveConversation(conversationId: string): Promise<boo
|
|||
*/
|
||||
export async function deleteConversation(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
// Lösche zuerst alle zugehörigen Nachrichten
|
||||
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;
|
||||
return await conversationApi.deleteConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Konversation:', error);
|
||||
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 {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
|
|
@ -13,37 +13,39 @@ export interface Document {
|
|||
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
|
||||
* @param conversationId Die ID der Konversation
|
||||
* @param content Der Inhalt des Dokuments
|
||||
* @returns Das erstellte Dokument oder null bei Fehler
|
||||
*/
|
||||
export async function createDocument(
|
||||
conversationId: string,
|
||||
content: string
|
||||
): Promise<Document | null> {
|
||||
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
|
||||
.from('documents')
|
||||
.insert({
|
||||
conversation_id: conversationId,
|
||||
version: 1, // Initiale Version ist immer 1
|
||||
content,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
const document = await documentApi.createDocument(conversationId, content);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Erstellen des Dokuments:', error);
|
||||
console.error('Vollständiger Fehler:', JSON.stringify(error));
|
||||
if (!document) {
|
||||
console.error('Fehler beim Erstellen des Dokuments');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Dokument erfolgreich erstellt:', data);
|
||||
return data as Document;
|
||||
console.log('Dokument erfolgreich erstellt:', document);
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Dokuments:', error);
|
||||
if (error instanceof Error) {
|
||||
|
|
@ -55,48 +57,20 @@ export async function createDocument(
|
|||
|
||||
/**
|
||||
* 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(
|
||||
conversationId: string,
|
||||
content: string
|
||||
): Promise<Document | null> {
|
||||
try {
|
||||
// Hole die aktuelle höchste Version
|
||||
const { data: latestVersionData, error: versionError } = await supabase
|
||||
.from('documents')
|
||||
.select('version')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
const document = await documentApi.createDocumentVersion(conversationId, content);
|
||||
|
||||
if (versionError) {
|
||||
console.error('Fehler beim Laden der letzten Dokumentversion:', versionError);
|
||||
if (!document) {
|
||||
console.error('Fehler beim Erstellen der neuen Dokumentversion');
|
||||
return null;
|
||||
}
|
||||
|
||||
const newVersion = (latestVersionData?.version || 0) + 1;
|
||||
|
||||
// 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;
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der neuen Dokumentversion:', error);
|
||||
return null;
|
||||
|
|
@ -105,32 +79,20 @@ export async function createDocumentVersion(
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
console.log(`Lade neuestes Dokument für Konversation ${conversationId}`);
|
||||
|
||||
// Einfache Abfrage ohne Cache-Busting
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
const document = await documentApi.getLatestDocument(conversationId);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
||||
if (error.code === 'PGRST116') {
|
||||
console.log('Kein Dokument gefunden (PGRST116)');
|
||||
}
|
||||
if (!document) {
|
||||
console.log('Kein Dokument gefunden');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`Neuestes Dokument gefunden: Version ${data.version}, ID ${data.id}`);
|
||||
return data as Document;
|
||||
console.log(`Neuestes Dokument gefunden: Version ${document.version}, ID ${document.id}`);
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
||||
return null;
|
||||
|
|
@ -139,35 +101,22 @@ export async function getLatestDocument(conversationId: string): Promise<Documen
|
|||
|
||||
/**
|
||||
* 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[]> {
|
||||
try {
|
||||
console.log(`Lade alle Dokumentversionen für Konversation ${conversationId}`);
|
||||
|
||||
// Einfache Abfrage ohne Cache-Busting (das verursacht Probleme)
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('version', { ascending: false });
|
||||
const documents = await documentApi.getAllDocumentVersions(conversationId);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
||||
return [];
|
||||
}
|
||||
console.log(`${documents.length} Dokumentversionen geladen`);
|
||||
|
||||
console.log(`${data?.length || 0} Dokumentversionen geladen`);
|
||||
|
||||
// Detaillierte Infos zur Fehlersuche
|
||||
if (data && data.length > 0) {
|
||||
console.log(`Erstes Dokument: ID=${data[0].id}, Version=${data[0].version}`);
|
||||
if (documents.length > 0) {
|
||||
console.log(`Erstes Dokument: ID=${documents[0].id}, Version=${documents[0].version}`);
|
||||
} else {
|
||||
console.log('Keine Dokumente gefunden');
|
||||
}
|
||||
|
||||
return data as Document[];
|
||||
return documents.map(toLocalDocument);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
||||
return [];
|
||||
|
|
@ -176,22 +125,10 @@ export async function getAllDocumentVersions(conversationId: string): Promise<Do
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
const { count, error } = await supabase
|
||||
.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;
|
||||
return await documentApi.hasDocument(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen auf Dokument:', error);
|
||||
return false;
|
||||
|
|
@ -200,47 +137,20 @@ export async function hasDocument(conversationId: string): Promise<boolean> {
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
console.log(`=== LÖSCH-OPERATION GESTARTET FÜR DOKUMENT ID ${documentId} ===`);
|
||||
|
||||
// Prüfe zuerst, ob das Dokument existiert
|
||||
const { data: existingData, error: checkError } = await supabase
|
||||
.from('documents')
|
||||
.select('id')
|
||||
.eq('id', documentId)
|
||||
.single();
|
||||
const success = await documentApi.deleteDocumentVersion(documentId);
|
||||
|
||||
if (checkError) {
|
||||
console.error('Fehler beim Prüfen, ob das Dokument existiert:', checkError);
|
||||
console.error('Details:', JSON.stringify(checkError));
|
||||
return false;
|
||||
if (success) {
|
||||
console.log(`=== DOKUMENT ${documentId} ERFOLGREICH GELÖSCHT ===`);
|
||||
} else {
|
||||
console.error('Fehler beim Löschen der Dokumentversion');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`=== DOKUMENT ${documentId} ERFOLGREICH GELÖSCHT ===`);
|
||||
return true;
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler beim Löschen der Dokumentversion:', 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 { sendChatRequest as sendChatRequestApi } from '../utils/api';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { chatApi, modelApi, usageApi, type ChatMessage, type TokenUsage } from './api';
|
||||
|
||||
// Typdefinition für eine Nachricht
|
||||
export type ChatMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
};
|
||||
// Re-export types for backward compatibility
|
||||
export type { ChatMessage };
|
||||
|
||||
// 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 = {
|
||||
id: string;
|
||||
choices: {
|
||||
// Für o3-mini-Modell
|
||||
content_filter_results?: any;
|
||||
finish_reason: string;
|
||||
index: number;
|
||||
|
|
@ -38,34 +39,20 @@ export type ChatResponse = {
|
|||
};
|
||||
};
|
||||
|
||||
// Token-Nutzungsinformationen
|
||||
export type TokenUsage = {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
|
||||
// Rückgabetyp für die Chat-Anfrage
|
||||
// Return type for chat request
|
||||
export type ChatRequestResult = {
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
};
|
||||
|
||||
// Backend-URL für sichere API-Aufrufe
|
||||
const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001';
|
||||
|
||||
// Logging der Konfiguration
|
||||
// Logging configuration
|
||||
console.log('Chat Service Konfiguration:', {
|
||||
backendUrl: BACKEND_URL,
|
||||
backendUrl: process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001',
|
||||
availableModels: availableModels.length,
|
||||
});
|
||||
|
||||
/**
|
||||
* Berechnet die geschätzten Kosten einer LLM-Anfrage
|
||||
* @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)
|
||||
* Calculates estimated cost for an LLM request
|
||||
*/
|
||||
export async function calculateTokenCost(
|
||||
promptTokens: number,
|
||||
|
|
@ -73,45 +60,31 @@ export async function calculateTokenCost(
|
|||
modelId: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
// Hole die Kosteninformationen aus dem Modell
|
||||
const { data: modelData, error } = await supabase
|
||||
.from('models')
|
||||
.select('cost_settings')
|
||||
.eq('id', modelId)
|
||||
.single();
|
||||
// Get cost settings from model
|
||||
const modelData = await modelApi.getModel(modelId);
|
||||
|
||||
if (error || !modelData || !modelData.cost_settings) {
|
||||
console.warn('Fehler beim Laden der Kosteninformationen, verwende Standardwerte:', error);
|
||||
// Standardwerte verwenden
|
||||
const promptCost = 0.0001; // pro 1K Tokens
|
||||
const completionCost = 0.0002; // pro 1K Tokens
|
||||
if (!modelData || !modelData.costSettings) {
|
||||
console.warn('Fehler beim Laden der Kosteninformationen, verwende Standardwerte');
|
||||
const promptCost = 0.0001;
|
||||
const completionCost = 0.0002;
|
||||
|
||||
// Berechne die Kosten
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
}
|
||||
|
||||
// Extrahiere die Kostensätze
|
||||
const promptCost = parseFloat(modelData.cost_settings.prompt_per_1k_tokens) || 0.0001;
|
||||
const completionCost = parseFloat(modelData.cost_settings.completion_per_1k_tokens) || 0.0002;
|
||||
const promptCost = modelData.costSettings.prompt_per_1k_tokens || 0.0001;
|
||||
const completionCost = modelData.costSettings.completion_per_1k_tokens || 0.0002;
|
||||
|
||||
// Berechne die Kosten
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Kostenberechnung:', error);
|
||||
// Fallback: vereinfachte Berechnung
|
||||
return Number(((promptTokens * 0.0001 + completionTokens * 0.0002) / 1000).toFixed(6));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert die Token-Nutzung in der Datenbank
|
||||
* @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
|
||||
* Logs token usage to the database
|
||||
*/
|
||||
export async function logTokenUsage(
|
||||
usage: TokenUsage,
|
||||
|
|
@ -121,36 +94,31 @@ export async function logTokenUsage(
|
|||
modelId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Berechne die geschätzten Kosten
|
||||
const estimatedCost = await calculateTokenCost(
|
||||
usage.prompt_tokens,
|
||||
usage.completion_tokens,
|
||||
modelId
|
||||
);
|
||||
|
||||
// Speichere die Nutzungsinformationen
|
||||
const { error } = await supabase
|
||||
.from('usage_logs')
|
||||
.insert({
|
||||
conversation_id: conversationId,
|
||||
message_id: messageId,
|
||||
user_id: userId,
|
||||
model_id: modelId,
|
||||
prompt_tokens: usage.prompt_tokens,
|
||||
completion_tokens: usage.completion_tokens,
|
||||
total_tokens: usage.total_tokens,
|
||||
estimated_cost: estimatedCost
|
||||
});
|
||||
const success = await usageApi.logTokenUsage({
|
||||
conversationId,
|
||||
messageId,
|
||||
modelId,
|
||||
promptTokens: usage.prompt_tokens,
|
||||
completionTokens: usage.completion_tokens,
|
||||
totalTokens: usage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Speichern der Token-Nutzung:', error);
|
||||
} else {
|
||||
if (success) {
|
||||
console.log('Token-Nutzung erfolgreich gespeichert:', {
|
||||
conversationId,
|
||||
messageId,
|
||||
totalTokens: usage.total_tokens,
|
||||
estimatedCost
|
||||
estimatedCost,
|
||||
});
|
||||
} else {
|
||||
console.error('Fehler beim Speichern der Token-Nutzung');
|
||||
}
|
||||
} catch (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
|
||||
* 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
|
||||
* Sends a chat request via the backend
|
||||
*/
|
||||
export async function sendChatRequest(
|
||||
messages: ChatMessage[],
|
||||
|
|
@ -172,57 +135,66 @@ export async function sendChatRequest(
|
|||
): Promise<string | ChatRequestResult> {
|
||||
console.log('sendChatRequest gestartet mit:', {
|
||||
messagesCount: messages.length,
|
||||
maxTokens
|
||||
maxTokens,
|
||||
});
|
||||
|
||||
try {
|
||||
// Hole aktuelle Modellparameter aus der Konversation (für Modellwechsel)
|
||||
let deployment = '';
|
||||
// Find model deployment from system message
|
||||
let modelId = '550e8400-e29b-41d4-a716-446655440000'; // Default to GPT-O3-Mini
|
||||
|
||||
// System-Nachricht mit Modell-Präfix suchen
|
||||
const systemMessage = messages.find(msg => msg.role === 'system' && msg.content.startsWith('MODEL:'));
|
||||
const systemMessage = messages.find(
|
||||
(msg) => msg.role === 'system' && msg.content.startsWith('MODEL:')
|
||||
);
|
||||
if (systemMessage) {
|
||||
deployment = systemMessage.content.split(':')[1].trim();
|
||||
const deployment = systemMessage.content.split(':')[1].trim();
|
||||
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 {
|
||||
console.warn('Keine System-Nachricht mit MODEL-Präfix gefunden!');
|
||||
}
|
||||
|
||||
// Falls kein Modell angegeben wurde, setze auf Fallback gpt-o3-mini-se
|
||||
const deploymentToUse = deployment || 'gpt-o3-mini-se';
|
||||
console.log('Verwende Deployment:', deploymentToUse);
|
||||
console.log('Verwende Model ID:', modelId);
|
||||
|
||||
// Konfiguration für den API-Wrapper (Backend benötigt nur deployment)
|
||||
const config = {
|
||||
deployment: deploymentToUse
|
||||
};
|
||||
// Filter out MODEL: system messages before sending to API
|
||||
const filteredMessages = messages.filter(
|
||||
(msg) => !(msg.role === 'system' && msg.content.startsWith('MODEL:'))
|
||||
);
|
||||
|
||||
// Verwende den zentralen API-Wrapper - dieser ruft das Backend auf
|
||||
const result = await sendChatRequestApi(messages, temperature, maxTokens, config);
|
||||
// Send request to backend
|
||||
const result = await chatApi.createCompletion({
|
||||
messages: filteredMessages,
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
});
|
||||
|
||||
// Wenn es ein einfacher String ist (Fehlerfall), diesen zurückgeben
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
if (!result) {
|
||||
return 'Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft.';
|
||||
}
|
||||
|
||||
// Ansonsten die vollständige Antwort mit Token-Nutzung zurückgeben
|
||||
return {
|
||||
content: result.content,
|
||||
usage: result.usage
|
||||
usage: result.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Chat-Anfrage:', error);
|
||||
|
||||
// Versuche, mehr Informationen über den Fehler zu erhalten
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerdetails:', {
|
||||
name: error.name,
|
||||
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'}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -24,190 +27,140 @@ export type SpaceMember = {
|
|||
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[]> {
|
||||
try {
|
||||
const { data: memberData, error: memberError } = await supabase
|
||||
.from('space_members')
|
||||
.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[];
|
||||
const spaces = await spaceApi.getUserSpaces();
|
||||
return spaces.map(toLocalSpace);
|
||||
} catch (error) {
|
||||
console.error('Error in getUserSpaces:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get spaces the user owns
|
||||
/**
|
||||
* Get spaces the user owns
|
||||
*/
|
||||
export async function getOwnedSpaces(userId: string): Promise<Space[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('spaces')
|
||||
.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[];
|
||||
const spaces = await spaceApi.getOwnedSpaces();
|
||||
return spaces.map(toLocalSpace);
|
||||
} catch (error) {
|
||||
console.error('Error in getOwnedSpaces:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get a single space by ID
|
||||
/**
|
||||
* Get a single space by ID
|
||||
*/
|
||||
export async function getSpace(spaceId: string): Promise<Space | null> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('spaces')
|
||||
.select('*')
|
||||
.eq('id', spaceId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching space:', error);
|
||||
const space = await spaceApi.getSpace(spaceId);
|
||||
if (!space) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as Space;
|
||||
return toLocalSpace(space);
|
||||
} catch (error) {
|
||||
console.error('Error in getSpace:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new space
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
export async function createSpace(
|
||||
userId: string,
|
||||
name: string,
|
||||
description?: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('spaces')
|
||||
.insert({
|
||||
name,
|
||||
description,
|
||||
owner_id: userId
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating space:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.id;
|
||||
const space = await spaceApi.createSpace(name, description);
|
||||
return space?.id || null;
|
||||
} catch (error) {
|
||||
console.error('Error in createSpace:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update a space
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
export async function updateSpace(
|
||||
spaceId: string,
|
||||
updates: { name?: string; description?: string; is_archived?: boolean }
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('spaces')
|
||||
.update(updates)
|
||||
.eq('id', spaceId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating space:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return await spaceApi.updateSpace(spaceId, {
|
||||
name: updates.name,
|
||||
description: updates.description,
|
||||
isArchived: updates.is_archived,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in updateSpace:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a space
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
export async function deleteSpace(spaceId: string): Promise<boolean> {
|
||||
try {
|
||||
// Delete the space (members will be cascade deleted due to foreign key constraint)
|
||||
const { error } = await supabase
|
||||
.from('spaces')
|
||||
.delete()
|
||||
.eq('id', spaceId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting space:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return await spaceApi.deleteSpace(spaceId);
|
||||
} catch (error) {
|
||||
console.error('Error in deleteSpace:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get members of a space
|
||||
/**
|
||||
* Get members of a space
|
||||
*/
|
||||
export async function getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.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[];
|
||||
const members = await spaceApi.getSpaceMembers(spaceId);
|
||||
return members.map(toLocalSpaceMember);
|
||||
} catch (error) {
|
||||
console.error('Error in getSpaceMembers:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Add a member to a space
|
||||
/**
|
||||
* Add a member to a space (invite)
|
||||
*/
|
||||
export async function inviteUserToSpace(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
|
|
@ -215,228 +168,83 @@ export async function inviteUserToSpace(
|
|||
role: 'admin' | 'member' | 'viewer' = 'member'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Check if user is already a member
|
||||
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;
|
||||
return await spaceApi.inviteUser(spaceId, userId, role);
|
||||
} catch (error) {
|
||||
console.error('Error in inviteUserToSpace:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Accept or decline a space invitation
|
||||
/**
|
||||
* Accept or decline a space invitation
|
||||
*/
|
||||
export async function respondToInvitation(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
status: 'accepted' | 'declined'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const updates: any = {
|
||||
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;
|
||||
return await spaceApi.respondToInvitation(spaceId, status);
|
||||
} catch (error) {
|
||||
console.error('Error in respondToInvitation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a member from a space
|
||||
/**
|
||||
* Remove a member from a space
|
||||
*/
|
||||
export async function removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('space_members')
|
||||
.delete()
|
||||
.eq('space_id', spaceId)
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error removing member:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return await spaceApi.removeMember(spaceId, userId);
|
||||
} catch (error) {
|
||||
console.error('Error in removeMember:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Change a member's role
|
||||
/**
|
||||
* Change a member's role
|
||||
*/
|
||||
export async function changeMemberRole(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
newRole: 'admin' | 'member' | 'viewer'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.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;
|
||||
return await spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||
} catch (error) {
|
||||
console.error('Error in changeMemberRole:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get user's role in a space
|
||||
/**
|
||||
* Get user's role in a space
|
||||
*/
|
||||
export async function getUserRoleInSpace(
|
||||
spaceId: string,
|
||||
userId: string
|
||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||
try {
|
||||
// First check if they're the owner
|
||||
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;
|
||||
return await spaceApi.getUserRoleInSpace(spaceId);
|
||||
} catch (error) {
|
||||
console.error('Error in getUserRoleInSpace:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get pending space invitations for a user
|
||||
export async function getPendingInvitations(userId: string): Promise<Array<{
|
||||
invitation: SpaceMember;
|
||||
space: Space;
|
||||
}>> {
|
||||
/**
|
||||
* Get pending space invitations for a user
|
||||
*/
|
||||
export async function getPendingInvitations(
|
||||
userId: string
|
||||
): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('space_members')
|
||||
.select(`
|
||||
*,
|
||||
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
|
||||
const invitations = await spaceApi.getPendingInvitations();
|
||||
return invitations.map((inv) => ({
|
||||
invitation: toLocalSpaceMember(inv.invitation),
|
||||
space: toLocalSpace(inv.space),
|
||||
}));
|
||||
} catch (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 {
|
||||
id: string;
|
||||
user_id: string;
|
||||
|
|
@ -19,25 +19,31 @@ export interface Template {
|
|||
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
|
||||
* @param userId Die ID des Benutzers
|
||||
* @returns Liste der Vorlagen
|
||||
*/
|
||||
export async function getTemplates(userId: string): Promise<Template[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('name');
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data as Template[];
|
||||
const templates = await templateApi.getTemplates();
|
||||
return templates.map(toLocalTemplate);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
return [];
|
||||
|
|
@ -46,23 +52,14 @@ export async function getTemplates(userId: string): Promise<Template[]> {
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.select('*')
|
||||
.eq('id', templateId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
const template = await templateApi.getTemplate(templateId);
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as Template;
|
||||
return toLocalTemplate(template);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
return null;
|
||||
|
|
@ -71,23 +68,27 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
|
|||
|
||||
/**
|
||||
* 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 {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert(template)
|
||||
.select()
|
||||
.single();
|
||||
const result = await templateApi.createTemplate({
|
||||
name: template.name,
|
||||
description: template.description || undefined,
|
||||
systemPrompt: template.system_prompt,
|
||||
initialQuestion: template.initial_question || undefined,
|
||||
modelId: template.model_id || undefined,
|
||||
color: template.color,
|
||||
documentMode: template.document_mode,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Erstellen der Vorlage:', error);
|
||||
if (!result) {
|
||||
console.error('Fehler beim Erstellen der Vorlage');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as Template;
|
||||
return toLocalTemplate(result);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Vorlage:', error);
|
||||
return null;
|
||||
|
|
@ -96,26 +97,25 @@ export async function createTemplate(template: Omit<Template, 'id' | 'created_at
|
|||
|
||||
/**
|
||||
* 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(
|
||||
templateId: string,
|
||||
updates: Partial<Omit<Template, 'id' | 'user_id' | 'created_at' | 'updated_at'>>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('templates')
|
||||
.update(updates)
|
||||
.eq('id', templateId);
|
||||
const apiUpdates: Parameters<typeof templateApi.updateTemplate>[1] = {};
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
if (updates.name !== undefined) apiUpdates.name = updates.name;
|
||||
if (updates.description !== undefined)
|
||||
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) {
|
||||
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
||||
return false;
|
||||
|
|
@ -124,22 +124,10 @@ export async function updateTemplate(
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('templates')
|
||||
.delete()
|
||||
.eq('id', templateId);
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Löschen der Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return await templateApi.deleteTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Vorlage:', error);
|
||||
return false;
|
||||
|
|
@ -148,31 +136,10 @@ export async function deleteTemplate(templateId: string): Promise<boolean> {
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
// Zuerst alle Vorlagen des Benutzers auf nicht-Standard setzen
|
||||
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;
|
||||
return await templateApi.setDefaultTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen der Standard-Vorlage:', error);
|
||||
return false;
|
||||
|
|
@ -181,24 +148,14 @@ export async function setDefaultTemplate(templateId: string, userId: string): Pr
|
|||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('is_default', true)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
||||
const template = await templateApi.getDefaultTemplate();
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as Template;
|
||||
return toLocalTemplate(template);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@chat/types": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "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
|
||||
// for information about these interfaces
|
||||
import type { SupabaseClient, Session, User } from '@supabase/supabase-js';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient;
|
||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>;
|
||||
// Auth is now handled client-side via Mana Core Auth
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
pathname?: string;
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,10 @@
|
|||
/**
|
||||
* 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 { createSupabaseServerClient } from '$lib/services/supabase';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Create Supabase client for this request
|
||||
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';
|
||||
},
|
||||
});
|
||||
return resolve(event);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
const BACKEND_URL = env.PUBLIC_BACKEND_URL || 'http://localhost:3001';
|
||||
const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3002';
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
type FetchOptions = {
|
||||
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
||||
body?: unknown;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export async function apiRequest<T>(
|
||||
async function fetchApi<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
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') };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}${endpoint}`, {
|
||||
...options,
|
||||
const response = await fetch(`${API_BASE}/api${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return { error: errorText || `HTTP ${response.status}` };
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
data: null,
|
||||
error: new Error(errorData.message || `API error: ${response.status}`),
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data };
|
||||
return { data, error: null };
|
||||
} 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 = {
|
||||
get: <T>(endpoint: string) => apiRequest<T>(endpoint, { method: 'GET' }),
|
||||
post: <T>(endpoint: string, body: unknown) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
put: <T>(endpoint: string, body: unknown) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
delete: <T>(endpoint: string) => apiRequest<T>(endpoint, { method: 'DELETE' }),
|
||||
// ============ Conversation API ============
|
||||
|
||||
export type Conversation = {
|
||||
id: string;
|
||||
userId: string;
|
||||
modelId: string;
|
||||
templateId?: string;
|
||||
spaceId?: string;
|
||||
title?: string;
|
||||
conversationMode: 'free' | 'guided' | 'template';
|
||||
documentMode: boolean;
|
||||
isArchived: boolean;
|
||||
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 type { ChatMessage, ChatCompletionResponse, AIModel } from '@chat/types';
|
||||
import { chatApi, modelApi, type ChatMessage, type ChatCompletionResponse, type Model } from './api';
|
||||
|
||||
export type { ChatMessage, ChatCompletionResponse };
|
||||
|
||||
export interface ChatCompletionRequest {
|
||||
messages: ChatMessage[];
|
||||
|
|
@ -16,31 +17,19 @@ export const chatService = {
|
|||
/**
|
||||
* Get available AI models
|
||||
*/
|
||||
async getModels(): Promise<AIModel[]> {
|
||||
const { data, error } = await api.get<AIModel[]>('/api/chat/models');
|
||||
if (error) {
|
||||
console.error('Failed to fetch models:', error);
|
||||
return [];
|
||||
}
|
||||
return data || [];
|
||||
async getModels(): Promise<Model[]> {
|
||||
return modelApi.getModels();
|
||||
},
|
||||
|
||||
/**
|
||||
* Send chat completion request
|
||||
*/
|
||||
async createCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse | null> {
|
||||
const { data, error } = await api.post<ChatCompletionResponse>('/api/chat/completions', {
|
||||
return chatApi.createCompletion({
|
||||
messages: request.messages,
|
||||
modelId: request.modelId,
|
||||
temperature: request.temperature ?? 0.7,
|
||||
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 { chatService } from './chat';
|
||||
import type { Conversation, Message, ChatMessage } from '@chat/types';
|
||||
import {
|
||||
conversationApi,
|
||||
chatApi,
|
||||
type Conversation,
|
||||
type Message,
|
||||
type ChatMessage,
|
||||
} from './api';
|
||||
|
||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
||||
|
||||
function getSupabase() {
|
||||
if (!supabase) {
|
||||
supabase = createSupabaseBrowserClient();
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
export type { Conversation, Message };
|
||||
|
||||
export const conversationService = {
|
||||
/**
|
||||
|
|
@ -25,116 +22,45 @@ export const conversationService = {
|
|||
mode: 'free' | 'guided' | 'template' = 'free',
|
||||
templateId?: string,
|
||||
documentMode: boolean = false,
|
||||
spaceId?: string
|
||||
spaceId?: string,
|
||||
): Promise<string | null> {
|
||||
const sb = getSupabase();
|
||||
const conversation = await conversationApi.createConversation({
|
||||
modelId,
|
||||
conversationMode: mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
const { data, error } = await sb
|
||||
.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;
|
||||
return conversation?.id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all active conversations for a user
|
||||
*/
|
||||
async getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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[];
|
||||
return conversationApi.getConversations(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get archived conversations
|
||||
*/
|
||||
async getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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[];
|
||||
return conversationApi.getArchivedConversations();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single conversation
|
||||
*/
|
||||
async getConversation(conversationId: string): Promise<Conversation | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return conversationApi.getConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get messages for a conversation
|
||||
*/
|
||||
async getMessages(conversationId: string): Promise<Message[]> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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[];
|
||||
return conversationApi.getMessages(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -143,114 +69,38 @@ export const conversationService = {
|
|||
async addMessage(
|
||||
conversationId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string
|
||||
messageText: string,
|
||||
): Promise<string | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
const message = await conversationApi.addMessage(conversationId, sender, messageText);
|
||||
return message?.id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update conversation title
|
||||
*/
|
||||
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return conversationApi.updateTitle(conversationId, title);
|
||||
},
|
||||
|
||||
/**
|
||||
* Archive a conversation
|
||||
*/
|
||||
async archiveConversation(conversationId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return conversationApi.archiveConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unarchive a conversation
|
||||
*/
|
||||
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return conversationApi.unarchiveConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a conversation permanently
|
||||
*/
|
||||
async deleteConversation(conversationId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
// 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;
|
||||
return conversationApi.deleteConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -259,7 +109,7 @@ export const conversationService = {
|
|||
async sendMessageAndGetResponse(
|
||||
conversationId: string,
|
||||
userMessage: string,
|
||||
modelId: string
|
||||
modelId: string,
|
||||
): Promise<{
|
||||
userMessageId: string | null;
|
||||
assistantMessageId: string | null;
|
||||
|
|
@ -275,11 +125,11 @@ export const conversationService = {
|
|||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||
role: m.sender === 'user' ? 'user' : m.sender === 'assistant' ? 'assistant' : 'system',
|
||||
content: m.message_text,
|
||||
content: m.messageText,
|
||||
}));
|
||||
|
||||
// Get AI response
|
||||
const response = await chatService.createCompletion({
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: chatMessages,
|
||||
modelId,
|
||||
});
|
||||
|
|
@ -293,14 +143,11 @@ export const conversationService = {
|
|||
}
|
||||
|
||||
// Save assistant message
|
||||
const assistantMessageId = await this.addMessage(conversationId, 'assistant', response.content);
|
||||
|
||||
// Update conversation timestamp
|
||||
const sb = getSupabase();
|
||||
await sb
|
||||
.from('conversations')
|
||||
.update({ updated_at: new Date().toISOString() })
|
||||
.eq('id', conversationId);
|
||||
const assistantMessageId = await this.addMessage(
|
||||
conversationId,
|
||||
'assistant',
|
||||
response.content,
|
||||
);
|
||||
|
||||
// Generate title if this is a new conversation (first or second message)
|
||||
let title: string | undefined;
|
||||
|
|
@ -325,7 +172,7 @@ export const conversationService = {
|
|||
async generateTitle(userMessage: string): Promise<string> {
|
||||
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 }],
|
||||
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
||||
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 type { Document, DocumentWithConversation } from '@chat/types';
|
||||
import { documentApi, conversationApi, type Document } from './api';
|
||||
|
||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
||||
export type { Document };
|
||||
|
||||
function getSupabase() {
|
||||
if (!supabase) {
|
||||
supabase = createSupabaseBrowserClient();
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
export type DocumentWithConversation = Document & {
|
||||
conversation_title: string;
|
||||
};
|
||||
|
||||
export const documentService = {
|
||||
/**
|
||||
* Get all documents for a user (latest version of each)
|
||||
* This requires fetching conversations first, then documents
|
||||
*/
|
||||
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
|
||||
const { data: conversations, error: convError } = await sb
|
||||
.from('conversations')
|
||||
.select('id, title, document_mode')
|
||||
.eq('user_id', userId)
|
||||
.eq('document_mode', true);
|
||||
// Filter to only document mode conversations
|
||||
const documentConversations = conversations.filter((c) => c.documentMode);
|
||||
|
||||
if (convError) {
|
||||
console.error('Error loading conversations:', convError);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!conversations || conversations.length === 0) {
|
||||
if (documentConversations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For each conversation, load the latest document version
|
||||
const documents: DocumentWithConversation[] = [];
|
||||
|
||||
for (const conv of conversations) {
|
||||
const { data: docData, error: docError } = await sb
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('conversation_id', conv.id)
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
for (const conv of documentConversations) {
|
||||
const doc = await documentApi.getLatestDocument(conv.id);
|
||||
|
||||
if (docError && docError.code !== 'PGRST116') {
|
||||
console.error(`Error loading document for conversation ${conv.id}:`, docError);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (docData) {
|
||||
if (doc) {
|
||||
documents.push({
|
||||
...docData,
|
||||
...doc,
|
||||
conversation_title: conv.title || 'Unbenannte Konversation',
|
||||
});
|
||||
}
|
||||
|
|
@ -69,108 +47,44 @@ export const documentService = {
|
|||
* Get the latest document for a conversation
|
||||
*/
|
||||
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return documentApi.getLatestDocument(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new document
|
||||
*/
|
||||
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return documentApi.createDocument(conversationId, content);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new version of a document
|
||||
*/
|
||||
async createDocumentVersion(conversationId: string, content: string): Promise<Document | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
// Get the current highest version
|
||||
const { data: latestVersionData, error: versionError } = await sb
|
||||
.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;
|
||||
async createDocumentVersion(
|
||||
conversationId: string,
|
||||
content: string,
|
||||
): Promise<Document | null> {
|
||||
return documentApi.createDocumentVersion(conversationId, content);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all versions of a document
|
||||
*/
|
||||
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||
const sb = getSupabase();
|
||||
return documentApi.getAllDocumentVersions(conversationId);
|
||||
},
|
||||
|
||||
const { data, error } = await sb
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('version', { ascending: false });
|
||||
/**
|
||||
* Check if a document exists for a conversation
|
||||
*/
|
||||
async hasDocument(conversationId: string): Promise<boolean> {
|
||||
return documentApi.hasDocument(conversationId);
|
||||
},
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading document versions:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data as Document[];
|
||||
/**
|
||||
* Delete a specific document version
|
||||
*/
|
||||
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||
return documentApi.deleteDocumentVersion(documentId);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,151 +1,60 @@
|
|||
/**
|
||||
* Space Service - CRUD operations via Supabase
|
||||
* Space Service - CRUD operations via Backend API
|
||||
*/
|
||||
|
||||
import { createSupabaseBrowserClient } from './supabase';
|
||||
import type { Space, SpaceMember, SpaceCreate, SpaceUpdate } from '@chat/types';
|
||||
import { spaceApi, type Space, type SpaceMember } from './api';
|
||||
|
||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
||||
|
||||
function getSupabase() {
|
||||
if (!supabase) {
|
||||
supabase = createSupabaseBrowserClient();
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
export type { Space, SpaceMember };
|
||||
|
||||
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[]> {
|
||||
const sb = getSupabase();
|
||||
|
||||
// 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[];
|
||||
return spaceApi.getUserSpaces();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single space by ID
|
||||
*/
|
||||
async getSpace(spaceId: string): Promise<Space | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return spaceApi.getSpace(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
async createSpace(space: SpaceCreate): Promise<string | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
const { data, error } = await sb
|
||||
.from('spaces')
|
||||
.insert({
|
||||
name: space.name,
|
||||
description: space.description,
|
||||
owner_id: space.owner_id,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating space:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.id;
|
||||
async createSpace(space: {
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
}): Promise<string | null> {
|
||||
const result = await spaceApi.createSpace(space.name, space.description);
|
||||
return result?.id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
async updateSpace(spaceId: string, updates: SpaceUpdate): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
const { error } = await sb.from('spaces').update(updates).eq('id', spaceId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating space:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
async updateSpace(
|
||||
spaceId: string,
|
||||
updates: { name?: string; description?: string; isArchived?: boolean },
|
||||
): Promise<boolean> {
|
||||
return spaceApi.updateSpace(spaceId, updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
async deleteSpace(spaceId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
const { error } = await sb.from('spaces').delete().eq('id', spaceId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting space:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return spaceApi.deleteSpace(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get members of a space
|
||||
*/
|
||||
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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[];
|
||||
return spaceApi.getSpaceMembers(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -153,62 +62,63 @@ export const spaceService = {
|
|||
*/
|
||||
async getUserRoleInSpace(
|
||||
spaceId: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
// 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;
|
||||
return spaceApi.getUserRoleInSpace(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Leave a space
|
||||
* Leave a space (remove self from members)
|
||||
*/
|
||||
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
return spaceApi.removeMember(spaceId, userId);
|
||||
},
|
||||
|
||||
const { error } = await sb
|
||||
.from('space_members')
|
||||
.delete()
|
||||
.eq('space_id', spaceId)
|
||||
.eq('user_id', userId);
|
||||
/**
|
||||
* Invite a user to a space
|
||||
*/
|
||||
async inviteUserToSpace(
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Respond to a space invitation
|
||||
*/
|
||||
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 type { Template, TemplateCreate, TemplateUpdate } from '@chat/types';
|
||||
import { templateApi, type Template } from './api';
|
||||
|
||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
||||
|
||||
function getSupabase() {
|
||||
if (!supabase) {
|
||||
supabase = createSupabaseBrowserClient();
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
export type { Template };
|
||||
|
||||
export const templateService = {
|
||||
/**
|
||||
* Get all templates for a user
|
||||
* Get all templates for the current user
|
||||
*/
|
||||
async getTemplates(userId: string): Promise<Template[]> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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[];
|
||||
return templateApi.getTemplates();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single template by ID
|
||||
*/
|
||||
async getTemplate(templateId: string): Promise<Template | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return templateApi.getTemplate(templateId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the default template for a user
|
||||
* Get the default template for the current user
|
||||
*/
|
||||
async getDefaultTemplate(userId: string): Promise<Template | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
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;
|
||||
return templateApi.getDefaultTemplate();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new template
|
||||
*/
|
||||
async createTemplate(template: TemplateCreate): Promise<Template | null> {
|
||||
const sb = getSupabase();
|
||||
|
||||
const { data, error } = await sb
|
||||
.from('templates')
|
||||
.insert(template)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating template:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as Template;
|
||||
async createTemplate(template: {
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion?: string;
|
||||
modelId?: string;
|
||||
color?: string;
|
||||
documentMode?: boolean;
|
||||
}): Promise<Template | null> {
|
||||
return templateApi.createTemplate({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
systemPrompt: template.systemPrompt,
|
||||
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> {
|
||||
const sb = getSupabase();
|
||||
|
||||
const { error } = await sb
|
||||
.from('templates')
|
||||
.update(updates)
|
||||
.eq('id', templateId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating template:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
async updateTemplate(
|
||||
templateId: string,
|
||||
updates: Partial<{
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion: string;
|
||||
modelId: string;
|
||||
color: string;
|
||||
documentMode: boolean;
|
||||
}>,
|
||||
): Promise<boolean> {
|
||||
return templateApi.updateTemplate(templateId, updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a template
|
||||
*/
|
||||
async deleteTemplate(templateId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
const { error } = await sb.from('templates').delete().eq('id', templateId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting template:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return templateApi.deleteTemplate(templateId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a template as default
|
||||
*/
|
||||
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||
const sb = getSupabase();
|
||||
|
||||
// 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;
|
||||
return templateApi.setDefaultTemplate(templateId);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,14 +3,26 @@
|
|||
* Now using Mana Core Auth instead of Supabase Auth
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
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 { 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
|
||||
let user = $state<UserData | null>(null);
|
||||
|
|
@ -38,6 +50,13 @@ export const authStore = {
|
|||
async initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
initialized = true;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
|
|
@ -58,6 +77,11 @@ export const authStore = {
|
|||
* Sign in with email and password
|
||||
*/
|
||||
async signIn(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
|
|
@ -80,6 +104,11 @@ export const authStore = {
|
|||
* Sign up with email and password
|
||||
*/
|
||||
async signUp(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server', needsVerification: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
|
|
@ -105,6 +134,12 @@ export const authStore = {
|
|||
* Sign out
|
||||
*/
|
||||
async signOut() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
user = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await authService.signOut();
|
||||
user = null;
|
||||
|
|
@ -119,6 +154,11 @@ export const authStore = {
|
|||
* Send password reset email
|
||||
*/
|
||||
async resetPassword(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
|
|
@ -137,6 +177,11 @@ export const authStore = {
|
|||
* Get user credit balance
|
||||
*/
|
||||
async getCredits() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const credits = await authService.getUserCredits();
|
||||
return credits;
|
||||
|
|
@ -150,6 +195,10 @@ export const authStore = {
|
|||
* Get access token for API calls
|
||||
*/
|
||||
async getAccessToken() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
return await authService.getAppToken();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { ChatLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
// Get redirect URL from query params
|
||||
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/chat');
|
||||
|
||||
// German translations
|
||||
const translations = {
|
||||
title: 'Anmelden',
|
||||
|
|
@ -47,7 +51,7 @@
|
|||
goto={goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/chat"
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#e0f2fe"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals, url }) => {
|
||||
const { session, user } = await locals.safeGetSession();
|
||||
|
||||
if (!session) {
|
||||
// Redirect to login with return URL
|
||||
const redirectTo = encodeURIComponent(url.pathname);
|
||||
redirect(303, `/login?redirectTo=${redirectTo}`);
|
||||
}
|
||||
|
||||
export const load: LayoutServerLoad = async ({ url }) => {
|
||||
// Return the current path for client-side redirect logic
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
pathname: url.pathname,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,11 +7,19 @@
|
|||
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
|
||||
// Set session from server data
|
||||
onMount(() => {
|
||||
if (data.session) {
|
||||
authStore.setSession(data.session);
|
||||
let isChecking = $state(true);
|
||||
|
||||
// Check auth on mount and redirect if not authenticated
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (!authStore.isAuthenticated) {
|
||||
const redirectTo = encodeURIComponent(data.pathname || '/chat');
|
||||
goto(`/login?redirectTo=${redirectTo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
isChecking = false;
|
||||
});
|
||||
|
||||
async function handleSignOut() {
|
||||
|
|
@ -20,93 +28,100 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Top Navigation -->
|
||||
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
ManaChat
|
||||
</a>
|
||||
<div class="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
<a
|
||||
href="/"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname === '/' || $page.url.pathname.startsWith('/chat')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Chat
|
||||
{#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">
|
||||
<!-- Top Navigation -->
|
||||
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
ManaChat
|
||||
</a>
|
||||
<div class="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
<a
|
||||
href="/"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname === '/' || $page.url.pathname.startsWith('/chat')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Chat
|
||||
</a>
|
||||
<a
|
||||
href="/templates"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/templates')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Templates
|
||||
</a>
|
||||
<a
|
||||
href="/spaces"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/spaces')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Spaces
|
||||
</a>
|
||||
<a
|
||||
href="/documents"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/documents')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Dokumente
|
||||
</a>
|
||||
<a
|
||||
href="/archive"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/archive')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Archiv
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
{#if authStore.user}
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
|
||||
{authStore.user.email}
|
||||
</span>
|
||||
{/if}
|
||||
<a
|
||||
href="/templates"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/templates')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
href="/profile"
|
||||
class="p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Profil"
|
||||
>
|
||||
Templates
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="/spaces"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/spaces')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
Spaces
|
||||
</a>
|
||||
<a
|
||||
href="/documents"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/documents')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Dokumente
|
||||
</a>
|
||||
<a
|
||||
href="/archive"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/archive')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Archiv
|
||||
</a>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
{#if data.user}
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
|
||||
{data.user.email}
|
||||
</span>
|
||||
{/if}
|
||||
<a
|
||||
href="/profile"
|
||||
class="p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Profil"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
</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_URL=http://localhost:3001
|
||||
|
||||
# Supabase Configuration (for database only, not auth)
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_SERVICE_KEY=your-service-key-here
|
||||
# PostgreSQL Database Configuration
|
||||
DATABASE_URL=postgresql://chat:password@localhost:5432/chat
|
||||
|
||||
# Server Configuration
|
||||
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",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"assets": [],
|
||||
"watchAssets": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,18 @@
|
|||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"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": {
|
||||
"@manacore/shared-errors": "workspace:*",
|
||||
|
|
@ -18,10 +29,13 @@
|
|||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"@supabase/supabase-js": "^2.47.10",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.3",
|
||||
"openai": "^4.77.0",
|
||||
"postgres": "^3.4.5",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
|
@ -40,6 +54,7 @@
|
|||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './db/database.module';
|
||||
import { ChatModule } from './chat/chat.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';
|
||||
|
||||
@Module({
|
||||
|
|
@ -10,8 +15,13 @@ import { HealthModule } from './health/health.module';
|
|||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
DatabaseModule,
|
||||
ChatModule,
|
||||
ConversationModule,
|
||||
TemplateModule,
|
||||
SpaceModule,
|
||||
DocumentModule,
|
||||
ModelModule,
|
||||
HealthModule,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export class ChatController {
|
|||
constructor(private readonly chatService: ChatService) {}
|
||||
|
||||
@Get('models')
|
||||
getModels() {
|
||||
async getModels() {
|
||||
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 { eq } from 'drizzle-orm';
|
||||
import {
|
||||
type AsyncResult,
|
||||
ok,
|
||||
|
|
@ -7,22 +8,11 @@ import {
|
|||
ValidationError,
|
||||
ServiceError,
|
||||
} 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';
|
||||
|
||||
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()
|
||||
export class ChatService {
|
||||
private readonly logger = new Logger(ChatService.name);
|
||||
|
|
@ -30,72 +20,55 @@ export class ChatService {
|
|||
private readonly endpoint: string;
|
||||
private readonly apiVersion: string;
|
||||
|
||||
// Available models configuration
|
||||
private readonly availableModels: AIModel[] = [
|
||||
{
|
||||
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) {
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||
) {
|
||||
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.apiVersion = this.configService.get<string>('AZURE_OPENAI_API_VERSION') || '2024-12-01-preview';
|
||||
this.endpoint =
|
||||
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) {
|
||||
this.logger.warn('AZURE_OPENAI_API_KEY is not set!');
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableModels(): AIModel[] {
|
||||
return this.availableModels;
|
||||
async getAvailableModels(): Promise<Model[]> {
|
||||
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 {
|
||||
return this.availableModels.find((m) => m.id === modelId);
|
||||
async getModelById(modelId: string): Promise<Model | undefined> {
|
||||
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(
|
||||
dto: ChatCompletionDto,
|
||||
userId?: string,
|
||||
): AsyncResult<ChatCompletionResponseDto> {
|
||||
const model = this.getModelById(dto.modelId);
|
||||
const model = await this.getModelById(dto.modelId);
|
||||
|
||||
if (!model) {
|
||||
return err(
|
||||
|
|
@ -110,9 +83,15 @@ export class ChatService {
|
|||
);
|
||||
}
|
||||
|
||||
const deployment = model.parameters.deployment;
|
||||
const temperature = dto.temperature ?? model.parameters.temperature;
|
||||
const maxTokens = dto.maxTokens ?? model.parameters.max_tokens;
|
||||
const params = model.parameters as {
|
||||
deployment?: string;
|
||||
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
|
||||
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 {
|
||||
ConversationService,
|
||||
type Conversation,
|
||||
type Message,
|
||||
} from './conversation.service';
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
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 {
|
||||
CurrentUser,
|
||||
|
|
@ -19,8 +27,27 @@ export class ConversationController {
|
|||
@Get()
|
||||
async getConversations(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Query('spaceId') spaceId?: string,
|
||||
): 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)) {
|
||||
throw result.error;
|
||||
|
|
@ -34,8 +61,10 @@ export class ConversationController {
|
|||
@Param('id') id: string,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Conversation> {
|
||||
// TODO: Add ownership check - ensure conversation belongs to user
|
||||
const result = await this.conversationService.getConversation(id);
|
||||
const result = await this.conversationService.getConversation(
|
||||
id,
|
||||
user.userId,
|
||||
);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
|
|
@ -49,8 +78,7 @@ export class ConversationController {
|
|||
@Param('id') id: string,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Message[]> {
|
||||
// TODO: Add ownership check - ensure conversation belongs to user
|
||||
const result = await this.conversationService.getMessages(id);
|
||||
const result = await this.conversationService.getMessages(id, user.userId);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
|
|
@ -61,13 +89,27 @@ export class ConversationController {
|
|||
|
||||
@Post()
|
||||
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,
|
||||
): Promise<Conversation> {
|
||||
const result = await this.conversationService.createConversation(
|
||||
user.userId,
|
||||
body.modelId,
|
||||
body.title,
|
||||
{
|
||||
title: body.title,
|
||||
templateId: body.templateId,
|
||||
conversationMode: body.conversationMode,
|
||||
documentMode: body.documentMode,
|
||||
spaceId: body.spaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isOk(result)) {
|
||||
|
|
@ -83,9 +125,9 @@ export class ConversationController {
|
|||
@Body() body: { sender: 'user' | 'assistant' | 'system'; messageText: string },
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Message> {
|
||||
// TODO: Add ownership check - ensure conversation belongs to user
|
||||
const result = await this.conversationService.addMessage(
|
||||
id,
|
||||
user.userId,
|
||||
body.sender,
|
||||
body.messageText,
|
||||
);
|
||||
|
|
@ -96,4 +138,74 @@ export class ConversationController {
|
|||
|
||||
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 { ConfigService } from '@nestjs/config';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { eq, and, desc, asc, sql } from 'drizzle-orm';
|
||||
import {
|
||||
type AsyncResult,
|
||||
ok,
|
||||
err,
|
||||
ServiceError,
|
||||
DatabaseError,
|
||||
NotFoundError,
|
||||
} from '@manacore/shared-errors';
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
title?: string;
|
||||
is_archived: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
message_text: string;
|
||||
created_at: string;
|
||||
}
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { type Database } from '../db/connection';
|
||||
import {
|
||||
conversations,
|
||||
type Conversation,
|
||||
type NewConversation,
|
||||
} from '../db/schema/conversations.schema';
|
||||
import { messages, type Message, type NewMessage } from '../db/schema/messages.schema';
|
||||
|
||||
@Injectable()
|
||||
export class ConversationService {
|
||||
private readonly logger = new Logger(ConversationService.name);
|
||||
private supabase: SupabaseClient | null = null;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL');
|
||||
const supabaseKey = this.configService.get<string>('SUPABASE_SERVICE_KEY');
|
||||
constructor(
|
||||
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||
) {}
|
||||
|
||||
if (supabaseUrl && supabaseKey) {
|
||||
this.supabase = createClient(supabaseUrl, supabaseKey);
|
||||
} else {
|
||||
this.logger.warn('Supabase configuration missing');
|
||||
}
|
||||
}
|
||||
async getConversations(
|
||||
userId: string,
|
||||
spaceId?: string,
|
||||
): AsyncResult<Conversation[]> {
|
||||
try {
|
||||
const conditions = [
|
||||
eq(conversations.userId, userId),
|
||||
eq(conversations.isArchived, false),
|
||||
];
|
||||
|
||||
async getConversations(userId: string): AsyncResult<Conversation[]> {
|
||||
if (!this.supabase) {
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
if (spaceId) {
|
||||
conditions.push(eq(conversations.spaceId, spaceId));
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
.from('conversations')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('is_archived', false)
|
||||
.order('updated_at', { ascending: false });
|
||||
const result = await this.db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(conversations.updatedAt));
|
||||
|
||||
if (error) {
|
||||
return ok(result);
|
||||
} catch (error) {
|
||||
this.logger.error('Error fetching conversations', error);
|
||||
return err(DatabaseError.queryFailed('Failed to fetch conversations'));
|
||||
}
|
||||
|
||||
return ok(data || []);
|
||||
}
|
||||
|
||||
async getConversation(id: string): AsyncResult<Conversation> {
|
||||
if (!this.supabase) {
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
async getArchivedConversations(userId: string): AsyncResult<Conversation[]> {
|
||||
try {
|
||||
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
|
||||
.from('conversations')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
async getConversation(id: string, userId: string): AsyncResult<Conversation> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(
|
||||
and(eq(conversations.id, id), eq(conversations.userId, userId)),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching conversation', error);
|
||||
if (error.code === 'PGRST116') {
|
||||
if (result.length === 0) {
|
||||
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 ok(data);
|
||||
}
|
||||
|
||||
async getMessages(conversationId: string): AsyncResult<Message[]> {
|
||||
if (!this.supabase) {
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
async getMessages(
|
||||
conversationId: string,
|
||||
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
|
||||
.from('messages')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversationId)
|
||||
.order('created_at', { ascending: true });
|
||||
const result = await this.db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(eq(messages.conversationId, conversationId))
|
||||
.orderBy(asc(messages.createdAt));
|
||||
|
||||
if (error) {
|
||||
return ok(result);
|
||||
} catch (error) {
|
||||
this.logger.error('Error fetching messages', error);
|
||||
return err(DatabaseError.queryFailed('Failed to fetch messages'));
|
||||
}
|
||||
|
||||
return ok(data || []);
|
||||
}
|
||||
|
||||
async createConversation(
|
||||
userId: string,
|
||||
modelId: string,
|
||||
title?: string,
|
||||
options?: {
|
||||
title?: string;
|
||||
templateId?: string;
|
||||
conversationMode?: 'free' | 'guided' | 'template';
|
||||
documentMode?: boolean;
|
||||
spaceId?: string;
|
||||
},
|
||||
): AsyncResult<Conversation> {
|
||||
if (!this.supabase) {
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
try {
|
||||
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
|
||||
.from('conversations')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
model_id: modelId,
|
||||
title: title || 'Neue Unterhaltung',
|
||||
is_archived: false,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
const result = await this.db
|
||||
.insert(conversations)
|
||||
.values(newConversation)
|
||||
.returning();
|
||||
|
||||
if (error) {
|
||||
return ok(result[0]);
|
||||
} catch (error) {
|
||||
this.logger.error('Error creating conversation', error);
|
||||
return err(DatabaseError.queryFailed('Failed to create conversation'));
|
||||
}
|
||||
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
async addMessage(
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string,
|
||||
): AsyncResult<Message> {
|
||||
if (!this.supabase) {
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
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
|
||||
.from('messages')
|
||||
.insert({
|
||||
conversation_id: conversationId,
|
||||
const newMessage: NewMessage = {
|
||||
conversationId,
|
||||
sender,
|
||||
message_text: messageText,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
messageText,
|
||||
};
|
||||
|
||||
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);
|
||||
return err(DatabaseError.queryFailed('Failed to add message'));
|
||||
}
|
||||
}
|
||||
|
||||
// Update conversation updated_at
|
||||
await this.supabase
|
||||
.from('conversations')
|
||||
.update({ updated_at: new Date().toISOString() })
|
||||
.eq('id', conversationId);
|
||||
async updateTitle(
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
title: 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);
|
||||
}
|
||||
|
||||
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 { ValidationPipe } from '@nestjs/common';
|
||||
import { AppExceptionFilter } from '@manacore/shared-errors/nestjs';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
|
|
@ -13,13 +12,14 @@ async function bootstrap() {
|
|||
'http://localhost:5173',
|
||||
'http://localhost:8081',
|
||||
'exp://localhost:8081',
|
||||
'http://localhost:3001', // Mana Core Auth
|
||||
],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Global exception filter for standardized error responses
|
||||
app.useGlobalFilters(new AppExceptionFilter());
|
||||
// Global exception filter will be added later via module
|
||||
// app.useGlobalFilters(new AppExceptionFilter());
|
||||
|
||||
// Enable validation
|
||||
app.useGlobalPipes(
|
||||
|
|
@ -33,7 +33,7 @@ async function bootstrap() {
|
|||
// Set global prefix for API routes
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
const port = process.env.PORT || 3001;
|
||||
const port = process.env.PORT || 3002;
|
||||
await app.listen(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": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"rootDir": "./src",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
|
|
@ -17,5 +19,7 @@
|
|||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": 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/storage": "^7.15.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/common": "^10.0.0",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
"dependencies": {
|
||||
"@manacore/shared-errors": "workspace:*",
|
||||
"@google/genai": "^1.14.0",
|
||||
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
|
||||
"@manacore/manadeck-database": "workspace:*",
|
||||
"@nestjs/axios": "^4.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:app": "turbo run dev --filter=@news/web --filter=@news/api",
|
||||
"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": {
|
||||
"prettier": "^3.3.3",
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,20 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Go-like error handling system for Manacore backends",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"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": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"type-check": "tsc --noEmit",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
|
||||
"@manacore/uload-database": "workspace:*",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue