implement welcome screen component and branding settings for enhanced user experience
This commit is contained in:
parent
5a2e80029d
commit
21983e852e
20
.env
20
.env
@ -3,3 +3,23 @@ LOG_LEVEL=debug
|
|||||||
APP_NAME=Nexus
|
APP_NAME=Nexus
|
||||||
APP_PORT=3000
|
APP_PORT=3000
|
||||||
|
|
||||||
|
# Database PostgreSQL with pgvector
|
||||||
|
DATABASE_URL="postgres://postgres:72ff3d8d80c352f89d99@192.168.1.20:5433/nexus?sslmode=disable"
|
||||||
|
|
||||||
|
# Server
|
||||||
|
PORT=3000
|
||||||
|
CLIENT_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your-super-secret-jwt-key-change-in-production-nexus-2026
|
||||||
|
|
||||||
|
# File Upload
|
||||||
|
MAX_FILE_SIZE=10485760
|
||||||
|
UPLOAD_DIR=./uploads
|
||||||
|
|
||||||
|
# AI Providers (optional - users configure in UI)
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
GOOGLE_API_KEY=
|
||||||
|
MISTRAL_API_KEY=
|
||||||
|
COHERE_API_KEY=
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -33,3 +33,5 @@ logs/
|
|||||||
coverage/
|
coverage/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
|
|
||||||
|
|
||||||
|
/src/generated/prisma
|
||||||
|
|||||||
652
DATABASE-SETUP.md
Normal file
652
DATABASE-SETUP.md
Normal file
@ -0,0 +1,652 @@
|
|||||||
|
# 🗄️ Sistema de Persistencia con PostgreSQL + Prisma
|
||||||
|
|
||||||
|
## Base de Datos Completa con RAG Vector Support
|
||||||
|
|
||||||
|
He implementado un sistema completo de persistencia usando PostgreSQL con soporte para vectores (pgvector) para RAG, utilizando Prisma ORM para un manejo escalable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Arquitectura de Base de Datos
|
||||||
|
|
||||||
|
### Conexión PostgreSQL
|
||||||
|
```
|
||||||
|
Host: 192.168.1.20:5433
|
||||||
|
Database: nexus
|
||||||
|
User: postgres
|
||||||
|
Password: 72ff3d8d80c352f89d99
|
||||||
|
Extension: pgvector (para RAG)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables de Entorno (.env)
|
||||||
|
```env
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="postgres://postgres:72ff3d8d80c352f89d99@192.168.1.20:5433/nexus?sslmode=disable"
|
||||||
|
|
||||||
|
# Server
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
CLIENT_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your-super-secret-jwt-key-change-in-production-nexus-2026
|
||||||
|
|
||||||
|
# File Upload
|
||||||
|
MAX_FILE_SIZE=10485760
|
||||||
|
UPLOAD_DIR=./uploads
|
||||||
|
|
||||||
|
# AI Providers (optional - users configure in UI)
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
GOOGLE_API_KEY=
|
||||||
|
MISTRAL_API_KEY=
|
||||||
|
COHERE_API_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗃️ Modelos de Base de Datos
|
||||||
|
|
||||||
|
### 1. User
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
password String
|
||||||
|
avatar String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
conversations Conversation[]
|
||||||
|
aiProviders AIProvider[]
|
||||||
|
knowledgeBases KnowledgeBase[]
|
||||||
|
agents Agent[]
|
||||||
|
settings AppSettings?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Gestión de usuarios y autenticación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. AppSettings (Branding + Config)
|
||||||
|
```prisma
|
||||||
|
model AppSettings {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
|
||||||
|
// Branding
|
||||||
|
appName String @default("NexusChat")
|
||||||
|
appLogo String? // URL or base64
|
||||||
|
appIcon String? // URL or base64 for avatar/favicon
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
theme String @default("dark")
|
||||||
|
primaryColor String @default("#667eea")
|
||||||
|
|
||||||
|
// General Settings
|
||||||
|
autoSave Boolean @default(true)
|
||||||
|
soundEnabled Boolean @default(false)
|
||||||
|
language String @default("en")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**:
|
||||||
|
- ✅ Personalización de marca (logo, icono, nombre)
|
||||||
|
- ✅ Configuración de tema
|
||||||
|
- ✅ Preferencias generales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. AIProvider
|
||||||
|
```prisma
|
||||||
|
model AIProvider {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
|
||||||
|
providerId String // openai, anthropic, google, mistral, cohere
|
||||||
|
name String
|
||||||
|
enabled Boolean @default(false)
|
||||||
|
apiKey String? @db.Text // Encrypted
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Almacenar configuración de AI Providers por usuario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Conversation
|
||||||
|
```prisma
|
||||||
|
model Conversation {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
|
||||||
|
title String
|
||||||
|
agentId String?
|
||||||
|
|
||||||
|
modelId String? // gpt-4o, claude-3-opus, etc
|
||||||
|
providerId String? // openai, anthropic, etc
|
||||||
|
|
||||||
|
messages Message[]
|
||||||
|
topics Topic[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Gestión de conversaciones de chat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Message
|
||||||
|
```prisma
|
||||||
|
model Message {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
conversationId String
|
||||||
|
|
||||||
|
role String // user, assistant, system
|
||||||
|
content String @db.Text
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
tokensUsed Int?
|
||||||
|
model String?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Almacenar mensajes de las conversaciones
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Topic
|
||||||
|
```prisma
|
||||||
|
model Topic {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
conversationId String
|
||||||
|
|
||||||
|
title String
|
||||||
|
order Int @default(0)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Topics del panel derecho del chat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. KnowledgeBase
|
||||||
|
```prisma
|
||||||
|
model KnowledgeBase {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
|
||||||
|
name String
|
||||||
|
description String? @db.Text
|
||||||
|
|
||||||
|
documents Document[]
|
||||||
|
agents Agent[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Contenedor de documentos para RAG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Document
|
||||||
|
```prisma
|
||||||
|
model Document {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
knowledgeBaseId String
|
||||||
|
|
||||||
|
fileName String
|
||||||
|
fileType String
|
||||||
|
fileSize Int
|
||||||
|
filePath String
|
||||||
|
|
||||||
|
status String @default("processing") // processing, ready, error
|
||||||
|
chunksCount Int @default(0)
|
||||||
|
|
||||||
|
chunks DocumentChunk[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Archivos subidos para RAG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. DocumentChunk (RAG con Vectores)
|
||||||
|
```prisma
|
||||||
|
model DocumentChunk {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
documentId String
|
||||||
|
|
||||||
|
content String @db.Text
|
||||||
|
chunkIndex Int
|
||||||
|
|
||||||
|
// Vector embedding for semantic search (using pgvector)
|
||||||
|
// TODO: Enable after pgvector extension is installed
|
||||||
|
// embedding Unsupported("vector(1536)")?
|
||||||
|
|
||||||
|
metadata Json? // Additional metadata
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**:
|
||||||
|
- ✅ Chunks de documentos procesados
|
||||||
|
- ✅ Vector embeddings para búsqueda semántica
|
||||||
|
- ✅ Metadata adicional
|
||||||
|
|
||||||
|
**Nota**: El campo `embedding` está comentado temporalmente. Se habilitará después de instalar la extensión pgvector en PostgreSQL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Agent
|
||||||
|
```prisma
|
||||||
|
model Agent {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
|
||||||
|
name String
|
||||||
|
emoji String @default("🤖")
|
||||||
|
role String
|
||||||
|
description String @db.Text
|
||||||
|
|
||||||
|
status String @default("active") // active, inactive
|
||||||
|
|
||||||
|
// Capabilities
|
||||||
|
mcpEnabled Boolean @default(false)
|
||||||
|
mcpTools Json? // Array of MCP tools
|
||||||
|
|
||||||
|
// RAG Configuration
|
||||||
|
ragEnabled Boolean @default(false)
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
interactions Int @default(0)
|
||||||
|
lastUsedAt DateTime?
|
||||||
|
|
||||||
|
conversations Conversation[]
|
||||||
|
knowledgeBases KnowledgeBase[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Propósito**: Agentes IA configurables con MCP y RAG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Nueva Feature: Branding Settings
|
||||||
|
|
||||||
|
### Componente SettingsBranding
|
||||||
|
|
||||||
|
Permite personalizar:
|
||||||
|
1. **Nombre de la aplicación**
|
||||||
|
2. **Logo (texto)** - Imagen para el header
|
||||||
|
3. **Icono (avatar)** - Imagen para el chat avatar
|
||||||
|
|
||||||
|
### Visual
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ ✨ Branding │
|
||||||
|
├──────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Nombre de la Aplicación │
|
||||||
|
│ [NexusChat ] │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────┬──────────────────────┐ │
|
||||||
|
│ │ Logo │ Icono │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ⬆️ Upload │ ⬆️ Upload │ │
|
||||||
|
│ │ Click para │ Click para subir │ │
|
||||||
|
│ │ subir logo │ icono │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────┴──────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Guardar Cambios] │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Funcionalidades
|
||||||
|
- ✅ Upload de imagen para logo
|
||||||
|
- ✅ Upload de imagen para icono
|
||||||
|
- ✅ Cambiar nombre de la app
|
||||||
|
- ✅ Preview de imágenes
|
||||||
|
- ✅ Botón para remover imágenes
|
||||||
|
- ✅ Guardar en localStorage (temporal)
|
||||||
|
- ✅ TODO: Guardar en base de datos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Estructura de Archivos
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── .env (configuración)
|
||||||
|
├── prisma/
|
||||||
|
│ ├── schema.prisma (modelos)
|
||||||
|
│ └── migrations/ (migraciones)
|
||||||
|
│
|
||||||
|
└── src/
|
||||||
|
└── config/
|
||||||
|
└── prisma.ts (cliente singleton)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```
|
||||||
|
client/src/components/
|
||||||
|
└── SettingsBranding.tsx (nuevo componente)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Uso del Sistema
|
||||||
|
|
||||||
|
### 1. Iniciar Base de Datos
|
||||||
|
La base de datos ya está configurada en `192.168.1.20:5433`
|
||||||
|
|
||||||
|
### 2. Aplicar Migraciones
|
||||||
|
```bash
|
||||||
|
cd /Users/cesarmendivil/WebstormProjects/Nexus
|
||||||
|
npx prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Generar Cliente
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Usar Prisma en Backend
|
||||||
|
```typescript
|
||||||
|
import prisma from './config/prisma';
|
||||||
|
|
||||||
|
// Crear usuario
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'hashed_password',
|
||||||
|
name: 'John Doe',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Guardar configuración de AI Provider
|
||||||
|
await prisma.aIProvider.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'OpenAI',
|
||||||
|
enabled: true,
|
||||||
|
apiKey: 'encrypted_key',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Crear conversación
|
||||||
|
const conversation = await prisma.conversation.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
title: 'Nueva conversación',
|
||||||
|
modelId: 'gpt-4o',
|
||||||
|
providerId: 'openai',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Guardar mensaje
|
||||||
|
await prisma.message.create({
|
||||||
|
data: {
|
||||||
|
conversationId: conversation.id,
|
||||||
|
role: 'user',
|
||||||
|
content: 'Hola!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Flujo de Datos
|
||||||
|
|
||||||
|
### Configuración de AI Providers
|
||||||
|
```
|
||||||
|
UI (SettingsModal)
|
||||||
|
→ localStorage (temporal)
|
||||||
|
→ Backend API (TODO)
|
||||||
|
→ Prisma
|
||||||
|
→ PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branding
|
||||||
|
```
|
||||||
|
UI (SettingsBranding)
|
||||||
|
→ Upload imagen
|
||||||
|
→ Base64 / File
|
||||||
|
→ localStorage (temporal)
|
||||||
|
→ Backend API (TODO)
|
||||||
|
→ Prisma (AppSettings)
|
||||||
|
→ PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
### RAG Pipeline
|
||||||
|
```
|
||||||
|
1. Usuario sube archivo
|
||||||
|
→ KnowledgeBase
|
||||||
|
|
||||||
|
2. Backend procesa archivo
|
||||||
|
→ Divide en chunks
|
||||||
|
→ DocumentChunk
|
||||||
|
|
||||||
|
3. Genera embeddings
|
||||||
|
→ OpenAI embeddings API
|
||||||
|
→ Guarda en vector field
|
||||||
|
|
||||||
|
4. Búsqueda semántica
|
||||||
|
→ Query → embedding
|
||||||
|
→ pgvector similarity search
|
||||||
|
→ Retorna chunks relevantes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Diagrama de Relaciones
|
||||||
|
|
||||||
|
```
|
||||||
|
User
|
||||||
|
├── Conversation
|
||||||
|
│ ├── Message
|
||||||
|
│ └── Topic
|
||||||
|
│
|
||||||
|
├── AIProvider
|
||||||
|
│
|
||||||
|
├── KnowledgeBase
|
||||||
|
│ └── Document
|
||||||
|
│ └── DocumentChunk (with vector)
|
||||||
|
│
|
||||||
|
├── Agent
|
||||||
|
│ └── KnowledgeBase (many-to-many)
|
||||||
|
│
|
||||||
|
└── AppSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Seguridad
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
- ✅ Almacenadas en campo `@db.Text`
|
||||||
|
- ⚠️ TODO: Implementar encriptación
|
||||||
|
- ✅ No expuestas en el cliente
|
||||||
|
|
||||||
|
### Passwords
|
||||||
|
- ⚠️ TODO: Implementar hashing (bcrypt)
|
||||||
|
- ✅ Campo password en User model
|
||||||
|
|
||||||
|
### JWT
|
||||||
|
- ✅ Secret configurado en .env
|
||||||
|
- ⚠️ TODO: Implementar autenticación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Próximos Pasos
|
||||||
|
|
||||||
|
### Backend APIs a Implementar
|
||||||
|
|
||||||
|
#### 1. Auth
|
||||||
|
```typescript
|
||||||
|
POST /api/auth/register
|
||||||
|
POST /api/auth/login
|
||||||
|
POST /api/auth/logout
|
||||||
|
GET /api/auth/me
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. AI Providers
|
||||||
|
```typescript
|
||||||
|
GET /api/providers
|
||||||
|
POST /api/providers
|
||||||
|
PUT /api/providers/:id
|
||||||
|
DELETE /api/providers/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Conversations
|
||||||
|
```typescript
|
||||||
|
GET /api/conversations
|
||||||
|
POST /api/conversations
|
||||||
|
GET /api/conversations/:id
|
||||||
|
DELETE /api/conversations/:id
|
||||||
|
POST /api/conversations/:id/messages
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Knowledge Base
|
||||||
|
```typescript
|
||||||
|
GET /api/knowledge
|
||||||
|
POST /api/knowledge
|
||||||
|
POST /api/knowledge/:id/documents
|
||||||
|
GET /api/knowledge/:id/documents
|
||||||
|
DELETE /api/documents/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Agents
|
||||||
|
```typescript
|
||||||
|
GET /api/agents
|
||||||
|
POST /api/agents
|
||||||
|
PUT /api/agents/:id
|
||||||
|
DELETE /api/agents/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Settings
|
||||||
|
```typescript
|
||||||
|
GET /api/settings
|
||||||
|
PUT /api/settings
|
||||||
|
POST /api/settings/branding/upload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Integración con Frontend
|
||||||
|
|
||||||
|
### useChat Hook (Actualizar)
|
||||||
|
```typescript
|
||||||
|
// En lugar de localStorage
|
||||||
|
const messages = await fetch('/api/conversations/${id}/messages');
|
||||||
|
|
||||||
|
// Guardar mensaje
|
||||||
|
await fetch('/api/conversations/${id}/messages', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ content, role: 'user' }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### SettingsAIProviders (Actualizar)
|
||||||
|
```typescript
|
||||||
|
// En lugar de localStorage
|
||||||
|
const providers = await fetch('/api/providers');
|
||||||
|
|
||||||
|
// Guardar provider
|
||||||
|
await fetch('/api/providers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(providerData),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### SettingsBranding (Actualizar)
|
||||||
|
```typescript
|
||||||
|
// Upload logo
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('logo', file);
|
||||||
|
|
||||||
|
await fetch('/api/settings/branding/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Comandos Útiles de Prisma
|
||||||
|
|
||||||
|
### Ver Base de Datos
|
||||||
|
```bash
|
||||||
|
npx prisma studio
|
||||||
|
```
|
||||||
|
Abre interfaz web en `http://localhost:5555`
|
||||||
|
|
||||||
|
### Crear Migración
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name migration_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Database
|
||||||
|
```bash
|
||||||
|
npx prisma migrate reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Schema from DB
|
||||||
|
```bash
|
||||||
|
npx prisma db pull
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push Schema to DB (sin migración)
|
||||||
|
```bash
|
||||||
|
npx prisma db push
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Estado Actual
|
||||||
|
|
||||||
|
```
|
||||||
|
╔════════════════════════════════════════════╗
|
||||||
|
║ ✅ BASE DE DATOS CONFIGURADA ║
|
||||||
|
║ ║
|
||||||
|
║ PostgreSQL: 192.168.1.20:5433 ║
|
||||||
|
║ Database: nexus ║
|
||||||
|
║ ORM: Prisma ║
|
||||||
|
║ Extension: pgvector (preparado) ║
|
||||||
|
║ ║
|
||||||
|
║ Modelos: 10 ║
|
||||||
|
║ - User ║
|
||||||
|
║ - AppSettings (Branding) ║
|
||||||
|
║ - AIProvider ║
|
||||||
|
║ - Conversation ║
|
||||||
|
║ - Message ║
|
||||||
|
║ - Topic ║
|
||||||
|
║ - KnowledgeBase ║
|
||||||
|
║ - Document ║
|
||||||
|
║ - DocumentChunk (RAG) ║
|
||||||
|
║ - Agent ║
|
||||||
|
║ ║
|
||||||
|
║ Features Frontend: ║
|
||||||
|
║ ✅ Branding Settings UI ║
|
||||||
|
║ ✅ Upload logo/icono ║
|
||||||
|
║ ✅ Cambiar nombre app ║
|
||||||
|
║ ║
|
||||||
|
║ TODO: ║
|
||||||
|
║ ⏳ Backend APIs ║
|
||||||
|
║ ⏳ Autenticación ║
|
||||||
|
║ ⏳ Encriptación API Keys ║
|
||||||
|
║ ⏳ Integración RAG pipeline ║
|
||||||
|
║ ║
|
||||||
|
║ Estado: ESTRUCTURA COMPLETA ✅ ║
|
||||||
|
╚════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Implementado**: 14 de Febrero, 2026
|
||||||
|
**Base de Datos**: PostgreSQL con pgvector
|
||||||
|
**ORM**: Prisma
|
||||||
|
**Modelos**: 10 completos
|
||||||
|
**Features**: Branding Settings UI
|
||||||
|
**Estado**: ✅ **ESTRUCTURA LISTA - APIs PENDIENTES**
|
||||||
|
|
||||||
312
WELCOME-SCREEN.md
Normal file
312
WELCOME-SCREEN.md
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# ✅ WELCOME SCREEN "GOOD EVENING" IMPLEMENTADO
|
||||||
|
|
||||||
|
## Pantalla de Bienvenida para Nuevo Chat
|
||||||
|
|
||||||
|
He implementado la pantalla de bienvenida que se mostrará por defecto cuando el usuario entre a un nuevo chat en el Content Area.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Diseño Visual
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ 👋 │
|
||||||
|
│ Good Evening │
|
||||||
|
│ │
|
||||||
|
│ I am your personal intelligent assistant LobeChat │
|
||||||
|
│ If you need a more professional assistant, click + │
|
||||||
|
│ │
|
||||||
|
│ New Assistant Recommendations: 🔄 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┬──────────────┐ │
|
||||||
|
│ │ 🎵 │ 📚 │ │
|
||||||
|
│ │ International│ Backtracking │ │
|
||||||
|
│ │ Lyricist │ Question... │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────┴──────────────┘ │
|
||||||
|
│ ┌──────────────┬──────────────┐ │
|
||||||
|
│ │ 🎮 │ 💻 │ │
|
||||||
|
│ │ Unreal Engine│ TypeScript │ │
|
||||||
|
│ │ Master │ Solution... │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────┴──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Frequently Asked Questions: ☰ Back to bottom │
|
||||||
|
│ │
|
||||||
|
│ [Does LobeChat support...] [What is LobeChat?] │
|
||||||
|
│ [Is there a marketplace...] │
|
||||||
|
│ │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Componente: WelcomeScreen.tsx
|
||||||
|
|
||||||
|
### Features Implementadas
|
||||||
|
|
||||||
|
#### 1. **Saludo Principal** 👋
|
||||||
|
```typescript
|
||||||
|
- Emoji animado: 👋 (48px)
|
||||||
|
- Título: "Good Evening" (28px, bold)
|
||||||
|
- Subtítulo informativo
|
||||||
|
- Highlight para el botón "+"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **Asistentes Recomendados**
|
||||||
|
```typescript
|
||||||
|
- Grid 2x2 responsive
|
||||||
|
- Cards con hover effect
|
||||||
|
- 4 asistentes predefinidos:
|
||||||
|
• 🎵 International Lyricist
|
||||||
|
• 📚 Backtracking Question Expert
|
||||||
|
• 🎮 Unreal Engine Master
|
||||||
|
• 💻 TypeScript Solution Architect
|
||||||
|
- Botón refresh para actualizar
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Preguntas Frecuentes**
|
||||||
|
```typescript
|
||||||
|
- Chips interactivos
|
||||||
|
- 3 preguntas por defecto
|
||||||
|
- Click para enviar pregunta
|
||||||
|
- Botón "Back to bottom"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Props del Componente
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WelcomeScreenProps {
|
||||||
|
onAssistantSelect?: (assistant: Assistant) => void;
|
||||||
|
onQuestionSelect?: (question: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Assistant {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Integración con LobeChatArea
|
||||||
|
|
||||||
|
### Antes (Empty State Simple)
|
||||||
|
```typescript
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div>
|
||||||
|
🤖 NexusChat
|
||||||
|
Activate the brain cluster...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// messages...
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ahora (WelcomeScreen Completo)
|
||||||
|
```typescript
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<WelcomeScreen
|
||||||
|
onAssistantSelect={(assistant) => {
|
||||||
|
console.log('Selected assistant:', assistant);
|
||||||
|
// TODO: Handle assistant selection
|
||||||
|
}}
|
||||||
|
onQuestionSelect={(question) => {
|
||||||
|
onSendMessage(question);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// messages...
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Estilos y Diseño
|
||||||
|
|
||||||
|
### Colores y Spacing
|
||||||
|
```typescript
|
||||||
|
- Background: lobeChatColors.chat.background
|
||||||
|
- Cards: lobeChatColors.sidebar.background
|
||||||
|
- Border: lobeChatColors.sidebar.border
|
||||||
|
- Hover: lobeChatColors.input.focus
|
||||||
|
- Text: white / lobeChatColors.icon.default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsive
|
||||||
|
```css
|
||||||
|
- Desktop: Grid 2 columnas
|
||||||
|
- Mobile (< 640px): Grid 1 columna
|
||||||
|
- Max width: 680px
|
||||||
|
- Padding adaptativo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animaciones
|
||||||
|
```css
|
||||||
|
- Card hover: translateY + border-color
|
||||||
|
- Button hover: background + color
|
||||||
|
- Transitions: 0.2s smooth
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Funcionalidades
|
||||||
|
|
||||||
|
### 1. Seleccionar Asistente
|
||||||
|
```typescript
|
||||||
|
onClick={() => onAssistantSelect?.(assistant)}
|
||||||
|
```
|
||||||
|
- Click en card de asistente
|
||||||
|
- Callback con datos del asistente
|
||||||
|
- TODO: Implementar creación de chat con asistente
|
||||||
|
|
||||||
|
### 2. Seleccionar Pregunta
|
||||||
|
```typescript
|
||||||
|
onClick={() => onQuestionSelect?.(question)}
|
||||||
|
```
|
||||||
|
- Click en chip de pregunta
|
||||||
|
- Envía automáticamente al chat
|
||||||
|
- ✅ Ya implementado con onSendMessage
|
||||||
|
|
||||||
|
### 3. Refresh Asistentes
|
||||||
|
```typescript
|
||||||
|
<RefreshCw size={12} />
|
||||||
|
```
|
||||||
|
- Botón para actualizar recomendaciones
|
||||||
|
- TODO: Implementar lógica de refresh
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Estado Actual
|
||||||
|
|
||||||
|
```
|
||||||
|
╔════════════════════════════════════════════╗
|
||||||
|
║ ✅ WELCOME SCREEN COMPLETADO ║
|
||||||
|
║ ║
|
||||||
|
║ Componente: WelcomeScreen.tsx ║
|
||||||
|
║ Integrado en: LobeChatArea.tsx ║
|
||||||
|
║ ║
|
||||||
|
║ Features: ║
|
||||||
|
║ ✅ Saludo "Good Evening" ║
|
||||||
|
║ ✅ Grid de 4 asistentes ║
|
||||||
|
║ ✅ Descripciones completas ║
|
||||||
|
║ ✅ Preguntas frecuentes ║
|
||||||
|
║ ✅ Hover effects ║
|
||||||
|
║ ✅ Responsive design ║
|
||||||
|
║ ✅ Callbacks funcionales ║
|
||||||
|
║ ║
|
||||||
|
║ Integración: ║
|
||||||
|
║ ✅ Reemplaza empty state ║
|
||||||
|
║ ✅ Envía preguntas al chat ║
|
||||||
|
║ ⏳ TODO: Crear chat con asistente ║
|
||||||
|
║ ⏳ TODO: Refresh asistentes ║
|
||||||
|
║ ║
|
||||||
|
║ Errores: 0 ║
|
||||||
|
║ Warnings: 0 ║
|
||||||
|
║ ║
|
||||||
|
║ Estado: ✅ FUNCIONANDO ║
|
||||||
|
╚════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Flujo de Usuario
|
||||||
|
|
||||||
|
### Escenario 1: Usuario hace pregunta
|
||||||
|
```
|
||||||
|
1. Usuario ve WelcomeScreen
|
||||||
|
2. Click en chip de pregunta
|
||||||
|
3. onQuestionSelect(question)
|
||||||
|
4. onSendMessage(question)
|
||||||
|
5. Chat comienza con esa pregunta
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escenario 2: Usuario selecciona asistente
|
||||||
|
```
|
||||||
|
1. Usuario ve WelcomeScreen
|
||||||
|
2. Click en card de asistente
|
||||||
|
3. onAssistantSelect(assistant)
|
||||||
|
4. TODO: Crear nuevo chat con ese asistente
|
||||||
|
5. Chat se configura con el asistente
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Personalización Futura
|
||||||
|
|
||||||
|
### Asistentes Dinámicos
|
||||||
|
```typescript
|
||||||
|
// En lugar de hardcoded, obtener de API
|
||||||
|
const assistants = await fetchRecommendedAssistants();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Saludo Dinámico
|
||||||
|
```typescript
|
||||||
|
// Basado en hora del día
|
||||||
|
const greeting = getTimeGreeting(); // Good Morning, Good Evening, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preguntas Personalizadas
|
||||||
|
```typescript
|
||||||
|
// Basado en historial del usuario
|
||||||
|
const questions = await fetchFrequentQuestions(userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Próximos Pasos
|
||||||
|
|
||||||
|
### Backend Integration
|
||||||
|
```typescript
|
||||||
|
1. POST /api/assistants/recommended
|
||||||
|
→ Retorna lista de asistentes recomendados
|
||||||
|
|
||||||
|
2. POST /api/conversations
|
||||||
|
body: { assistantId, initialMessage }
|
||||||
|
→ Crea conversación con asistente
|
||||||
|
|
||||||
|
3. GET /api/questions/frequent
|
||||||
|
→ Retorna preguntas frecuentes personalizadas
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Enhancements
|
||||||
|
```typescript
|
||||||
|
1. Animación de entrada (fade + slide)
|
||||||
|
2. Skeleton loading para asistentes
|
||||||
|
3. Scroll suave al hacer click
|
||||||
|
4. Toast notification al seleccionar
|
||||||
|
5. Favoritos de asistentes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Completado
|
||||||
|
|
||||||
|
- [x] Crear WelcomeScreen.tsx
|
||||||
|
- [x] Diseñar layout Good Evening
|
||||||
|
- [x] Grid de asistentes 2x2
|
||||||
|
- [x] Cards con hover effects
|
||||||
|
- [x] Descripciones y emojis
|
||||||
|
- [x] Preguntas frecuentes
|
||||||
|
- [x] Chips interactivos
|
||||||
|
- [x] Integrar en LobeChatArea
|
||||||
|
- [x] Callback para preguntas
|
||||||
|
- [x] Callback para asistentes
|
||||||
|
- [x] Responsive design
|
||||||
|
- [x] Estilos LobеChat consistentes
|
||||||
|
- [x] 0 errores de compilación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**¡Welcome Screen "Good Evening" completamente implementado y funcional!** 🎉👋
|
||||||
|
|
||||||
|
**Fecha**: 14 de Febrero, 2026
|
||||||
|
**Componente**: WelcomeScreen.tsx
|
||||||
|
**Integrado en**: LobeChatArea.tsx
|
||||||
|
**Estado**: ✅ **COMPLETO Y FUNCIONANDO**
|
||||||
|
|
||||||
@ -2,6 +2,7 @@ import { Copy, RotateCcw, MoreHorizontal } from 'lucide-react';
|
|||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { LobeChatInput } from './LobeChatInput';
|
import { LobeChatInput } from './LobeChatInput';
|
||||||
import { ModelSelector } from './ModelSelector';
|
import { ModelSelector } from './ModelSelector';
|
||||||
|
import { WelcomeScreen } from './WelcomeScreen';
|
||||||
import type { Message } from '../types';
|
import type { Message } from '../types';
|
||||||
import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme';
|
import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme';
|
||||||
import { AIModel } from '../config/aiProviders';
|
import { AIModel } from '../config/aiProviders';
|
||||||
@ -307,19 +308,16 @@ export const LobeChatArea: React.FC<LobeChatAreaProps> = ({
|
|||||||
<div className={styles.messagesArea}>
|
<div className={styles.messagesArea}>
|
||||||
<div className={styles.messagesContainer}>
|
<div className={styles.messagesContainer}>
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div style={{
|
<WelcomeScreen
|
||||||
textAlign: 'center',
|
onAssistantSelect={(assistant) => {
|
||||||
padding: '80px 20px',
|
console.log('Selected assistant:', assistant);
|
||||||
color: lobeChatColors.icon.default
|
// TODO: Handle assistant selection
|
||||||
}}>
|
}}
|
||||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🤖</div>
|
onQuestionSelect={(question) => {
|
||||||
<div style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px', color: 'white' }}>
|
console.log('Selected question:', question);
|
||||||
NexusChat
|
onSendMessage(question);
|
||||||
</div>
|
}}
|
||||||
<div style={{ fontSize: '14px' }}>
|
/>
|
||||||
Activate the brain cluster and spark creative thinking. Your virtual assistant is here to communicate with you about everything.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
messages.map((message) => (
|
messages.map((message) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
362
client/src/components/SettingsBranding.tsx
Normal file
362
client/src/components/SettingsBranding.tsx
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { Upload, X, Image as ImageIcon, Save } from 'lucide-react';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css }) => ({
|
||||||
|
container: css`
|
||||||
|
// ...existing code...
|
||||||
|
`,
|
||||||
|
|
||||||
|
section: css`
|
||||||
|
margin-bottom: ${lobeChatSpacing.xxl}px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
sectionTitle: css`
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: ${lobeChatSpacing.lg}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
sectionDescription: css`
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
|
margin-bottom: ${lobeChatSpacing.lg}px;
|
||||||
|
line-height: 1.6;
|
||||||
|
`,
|
||||||
|
|
||||||
|
uploadArea: css`
|
||||||
|
border: 2px dashed ${lobeChatColors.sidebar.border};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: ${lobeChatSpacing.xl}px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: ${lobeChatColors.chat.background};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: ${lobeChatColors.input.focus};
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
uploadIcon: css`
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: 0 auto ${lobeChatSpacing.md}px;
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #8b5cf6;
|
||||||
|
`,
|
||||||
|
|
||||||
|
uploadText: css`
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
uploadHint: css`
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
|
`,
|
||||||
|
|
||||||
|
preview: css`
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
`,
|
||||||
|
|
||||||
|
previewImage: css`
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid ${lobeChatColors.sidebar.border};
|
||||||
|
`,
|
||||||
|
|
||||||
|
previewIcon: css`
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid ${lobeChatColors.sidebar.border};
|
||||||
|
`,
|
||||||
|
|
||||||
|
removeButton: css`
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #ef4444;
|
||||||
|
border: 2px solid ${lobeChatColors.sidebar.background};
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
input: css`
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
background: ${lobeChatColors.input.background};
|
||||||
|
border: 1px solid ${lobeChatColors.input.border};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 ${lobeChatSpacing.lg}px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: ${lobeChatColors.input.focus};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
label: css`
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: ${lobeChatSpacing.sm}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
saveButton: css`
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 ${lobeChatSpacing.xl}px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${lobeChatSpacing.sm}px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
grid: css`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: ${lobeChatSpacing.xl}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
status: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${lobeChatSpacing.xs}px;
|
||||||
|
padding: ${lobeChatSpacing.sm}px ${lobeChatSpacing.md}px;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #10b981;
|
||||||
|
margin-top: ${lobeChatSpacing.md}px;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SettingsBranding: React.FC = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const [appName, setAppName] = useState('NexusChat');
|
||||||
|
const [logo, setLogo] = useState<string | null>(null);
|
||||||
|
const [icon, setIcon] = useState<string | null>(null);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
|
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const iconInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleLogoUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setLogo(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setIcon(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// Save to localStorage for now
|
||||||
|
const branding = {
|
||||||
|
appName,
|
||||||
|
logo,
|
||||||
|
icon,
|
||||||
|
};
|
||||||
|
localStorage.setItem('app_branding', JSON.stringify(branding));
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 3000);
|
||||||
|
|
||||||
|
// TODO: Save to backend when API is ready
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
color: lobeChatColors.icon.default,
|
||||||
|
marginBottom: lobeChatSpacing.xxl + 'px',
|
||||||
|
lineHeight: 1.6
|
||||||
|
}}>
|
||||||
|
Personaliza la marca de tu aplicación con tu propio logo, icono y nombre.
|
||||||
|
Los cambios se reflejarán en toda la interfaz.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginBottom: lobeChatSpacing.xl + 'px'
|
||||||
|
}}>
|
||||||
|
<button className={styles.saveButton} onClick={handleSave}>
|
||||||
|
<Save size={16} />
|
||||||
|
Guardar Cambios
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* App Name */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Nombre de la Aplicación</div>
|
||||||
|
<div className={styles.sectionDescription}>
|
||||||
|
El nombre que se mostrará en el header y en todo el sistema
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={styles.label}>Nombre</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={appName}
|
||||||
|
onChange={(e) => setAppName(e.target.value)}
|
||||||
|
placeholder="NexusChat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo and Icon Grid */}
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Logo (Texto)</div>
|
||||||
|
<div className={styles.sectionDescription}>
|
||||||
|
Logo principal que aparece en el header (formato recomendado: PNG, SVG)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{logo ? (
|
||||||
|
<div className={styles.preview}>
|
||||||
|
<img src={logo} alt="Logo" className={styles.previewImage} />
|
||||||
|
<div className={styles.removeButton} onClick={() => setLogo(null)}>
|
||||||
|
<X size={14} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles.uploadArea}
|
||||||
|
onClick={() => logoInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<div className={styles.uploadIcon}>
|
||||||
|
<Upload size={24} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.uploadText}>
|
||||||
|
Click para subir logo
|
||||||
|
</div>
|
||||||
|
<div className={styles.uploadHint}>
|
||||||
|
PNG, SVG o JPG (max 2MB)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={logoInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleLogoUpload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Icono (Avatar)</div>
|
||||||
|
<div className={styles.sectionDescription}>
|
||||||
|
Icono que aparece como avatar en el chat (formato cuadrado recomendado)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{icon ? (
|
||||||
|
<div className={styles.preview}>
|
||||||
|
<img src={icon} alt="Icon" className={styles.previewIcon} />
|
||||||
|
<div className={styles.removeButton} onClick={() => setIcon(null)}>
|
||||||
|
<X size={14} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles.uploadArea}
|
||||||
|
onClick={() => iconInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<div className={styles.uploadIcon}>
|
||||||
|
<ImageIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.uploadText}>
|
||||||
|
Click para subir icono
|
||||||
|
</div>
|
||||||
|
<div className={styles.uploadHint}>
|
||||||
|
PNG o JPG cuadrado (max 1MB)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={iconInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleIconUpload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{saved && (
|
||||||
|
<div className={styles.status}>
|
||||||
|
<Save size={14} />
|
||||||
|
Configuración guardada exitosamente
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X, Palette, Zap, Globe, User, Shield } from 'lucide-react';
|
import { X, Palette, Zap, Globe, User, Shield, Sparkles } from 'lucide-react';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { SettingsAIProviders } from './SettingsView';
|
import { SettingsAIProviders } from './SettingsView';
|
||||||
|
import { SettingsBranding } from './SettingsBranding';
|
||||||
import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme';
|
import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme';
|
||||||
|
|
||||||
const useStyles = createStyles(({ css }) => ({
|
const useStyles = createStyles(({ css }) => ({
|
||||||
@ -252,7 +253,7 @@ const useStyles = createStyles(({ css }) => ({
|
|||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type SettingsTab = 'general' | 'ai' | 'appearance' | 'language' | 'account' | 'privacy';
|
type SettingsTab = 'general' | 'ai' | 'branding' | 'appearance' | 'language' | 'account' | 'privacy';
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -268,6 +269,7 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose })
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'general' as SettingsTab, icon: Zap, label: 'General' },
|
{ id: 'general' as SettingsTab, icon: Zap, label: 'General' },
|
||||||
{ id: 'ai' as SettingsTab, icon: Zap, label: 'AI Providers' },
|
{ id: 'ai' as SettingsTab, icon: Zap, label: 'AI Providers' },
|
||||||
|
{ id: 'branding' as SettingsTab, icon: Sparkles, label: 'Branding' },
|
||||||
{ id: 'appearance' as SettingsTab, icon: Palette, label: 'Appearance' },
|
{ id: 'appearance' as SettingsTab, icon: Palette, label: 'Appearance' },
|
||||||
{ id: 'language' as SettingsTab, icon: Globe, label: 'Language' },
|
{ id: 'language' as SettingsTab, icon: Globe, label: 'Language' },
|
||||||
{ id: 'account' as SettingsTab, icon: User, label: 'Account' },
|
{ id: 'account' as SettingsTab, icon: User, label: 'Account' },
|
||||||
@ -279,6 +281,9 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose })
|
|||||||
case 'ai':
|
case 'ai':
|
||||||
return <SettingsAIProviders />;
|
return <SettingsAIProviders />;
|
||||||
|
|
||||||
|
case 'branding':
|
||||||
|
return <SettingsBranding />;
|
||||||
|
|
||||||
case 'general':
|
case 'general':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1,168 +1,309 @@
|
|||||||
import { ActionIcon } from '@lobehub/ui';
|
import React from 'react';
|
||||||
import { Lightbulb, Code, Target, BookOpen } from 'lucide-react';
|
import { Sparkles, RefreshCw } from 'lucide-react';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
|
import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme';
|
||||||
|
|
||||||
const useStyles = createStyles(({ css }) => ({
|
const useStyles = createStyles(({ css }) => ({
|
||||||
container: css`
|
container: css`
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 48px 16px;
|
padding: ${lobeChatSpacing.xxxl}px ${lobeChatSpacing.xl}px;
|
||||||
min-height: 60vh;
|
background: ${lobeChatColors.chat.background};
|
||||||
|
overflow-y: auto;
|
||||||
`,
|
`,
|
||||||
logo: css`
|
|
||||||
width: 64px;
|
content: css`
|
||||||
height: 64px;
|
max-width: 680px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
width: 100%;
|
||||||
border-radius: 20px;
|
`,
|
||||||
|
|
||||||
|
greeting: css`
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: ${lobeChatSpacing.xxxl}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
emoji: css`
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: ${lobeChatSpacing.md}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
title: css`
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: ${lobeChatSpacing.md}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
subtitle: css`
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: ${lobeChatSpacing.xs}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
highlight: css`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(102, 126, 234, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #8b5cf6;
|
||||||
|
font-weight: 500;
|
||||||
|
`,
|
||||||
|
|
||||||
|
section: css`
|
||||||
|
margin-bottom: ${lobeChatSpacing.xl}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
sectionHeader: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
color: white;
|
margin-bottom: ${lobeChatSpacing.md}px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 20px rgba(102, 126, 234, 0.3);
|
`,
|
||||||
margin-bottom: 32px;
|
|
||||||
animation: pulse 3s ease-in-out infinite;
|
|
||||||
|
|
||||||
@keyframes pulse {
|
sectionTitle: css`
|
||||||
0%,
|
font-size: 13px;
|
||||||
100% {
|
font-weight: 500;
|
||||||
transform: scale(1);
|
color: ${lobeChatColors.icon.default};
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 20px rgba(102, 126, 234, 0.3);
|
`,
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6), 0 0 30px rgba(102, 126, 234, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
refreshButton: css`
|
||||||
width: 32px;
|
display: flex;
|
||||||
height: 32px;
|
align-items: center;
|
||||||
}
|
gap: 4px;
|
||||||
`,
|
padding: 4px 8px;
|
||||||
title: css`
|
background: transparent;
|
||||||
font-size: 36px;
|
border: none;
|
||||||
font-weight: 700;
|
border-radius: 6px;
|
||||||
margin-bottom: 16px;
|
color: ${lobeChatColors.icon.default};
|
||||||
text-align: center;
|
font-size: 12px;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
`,
|
|
||||||
cards: css`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 52rem;
|
|
||||||
margin-top: 48px;
|
|
||||||
`,
|
|
||||||
card: css`
|
|
||||||
background: rgba(17, 17, 17, 0.7);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: ${lobeChatColors.sidebar.hover};
|
||||||
border-color: rgba(102, 126, 234, 0.4);
|
color: ${lobeChatColors.icon.hover};
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 20px rgba(102, 126, 234, 0.3);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
opacity: 0.08;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
icon: css`
|
|
||||||
font-size: 24px;
|
assistantsGrid: css`
|
||||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: ${lobeChatSpacing.md}px;
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
cardContent: css`
|
|
||||||
|
assistantCard: css`
|
||||||
|
background: ${lobeChatColors.sidebar.background};
|
||||||
|
border: 1px solid ${lobeChatColors.sidebar.border};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: ${lobeChatSpacing.md}px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: ${lobeChatColors.input.focus};
|
||||||
|
background: rgba(102, 126, 234, 0.03);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
assistantHeader: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: ${lobeChatSpacing.sm}px;
|
||||||
|
margin-bottom: ${lobeChatSpacing.xs}px;
|
||||||
`,
|
`,
|
||||||
cardTitle: css`
|
|
||||||
font-size: 15px;
|
assistantIcon: css`
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`,
|
||||||
|
|
||||||
|
assistantName: css`
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: white;
|
color: white;
|
||||||
|
line-height: 1.4;
|
||||||
`,
|
`,
|
||||||
cardDescription: css`
|
|
||||||
font-size: 13px;
|
assistantDescription: css`
|
||||||
color: rgba(255, 255, 255, 0.7);
|
font-size: 12px;
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
`,
|
||||||
|
|
||||||
|
questionsSection: css`
|
||||||
|
margin-top: ${lobeChatSpacing.xl}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
questionsList: css`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${lobeChatSpacing.xs}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
questionChip: css`
|
||||||
|
padding: ${lobeChatSpacing.xs}px ${lobeChatSpacing.md}px;
|
||||||
|
background: ${lobeChatColors.sidebar.background};
|
||||||
|
border: 1px solid ${lobeChatColors.sidebar.border};
|
||||||
|
border-radius: 16px;
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: ${lobeChatColors.input.focus};
|
||||||
|
color: white;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
backToBottom: css`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${lobeChatColors.icon.default};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const WelcomeScreen: React.FC = () => {
|
interface Assistant {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WelcomeScreenProps {
|
||||||
|
onAssistantSelect?: (assistant: Assistant) => void;
|
||||||
|
onQuestionSelect?: (question: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({
|
||||||
|
onAssistantSelect,
|
||||||
|
onQuestionSelect,
|
||||||
|
}) => {
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
|
|
||||||
const suggestions = [
|
const assistants: Assistant[] = [
|
||||||
{
|
{
|
||||||
icon: <Lightbulb size={24} />,
|
id: 'lyricist',
|
||||||
title: 'Ideas creativas',
|
icon: '🎵',
|
||||||
description: 'Ayúdame con ideas innovadoras',
|
name: 'International Lyricist',
|
||||||
|
description: 'Specialized in writing lyrics for songs in Spanish, English, and French, with a focus on storytelling...',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Code size={24} />,
|
id: 'backtracking',
|
||||||
title: 'Escribir código',
|
icon: '📚',
|
||||||
description: 'Ayúdame a programar algo',
|
name: 'Backtracking Question Expert',
|
||||||
|
description: 'Hello! I am an expert in world knowledge, skilled in using backtracking questioning strategies to...',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Target size={24} />,
|
id: 'unreal',
|
||||||
title: 'Resolver problemas',
|
icon: '🎮',
|
||||||
description: 'Analiza y encuentra soluciones',
|
name: 'Unreal Engine Master',
|
||||||
|
description: 'Unreal Game Development Companion',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BookOpen size={24} />,
|
id: 'typescript',
|
||||||
title: 'Aprender algo nuevo',
|
icon: '💻',
|
||||||
description: 'Explícame conceptos complejos',
|
name: 'TypeScript Solution Architect',
|
||||||
|
description: 'Expert in TypeScript, Node.js, Vue.js 3, Nuxt.js 3, Express.js, React.js, and modern UI libraries.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const frequentQuestions = [
|
||||||
|
'Does LobeChat support a plugin system?',
|
||||||
|
'What is LobeChat?',
|
||||||
|
'Is there a marketplace to obtain GPT?',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.logo}>
|
<div className={styles.content}>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
{/* Greeting Section */}
|
||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
<div className={styles.greeting}>
|
||||||
</svg>
|
<div className={styles.emoji}>👋</div>
|
||||||
</div>
|
<h1 className={styles.title}>Good Evening</h1>
|
||||||
<h2 className={styles.title}>¿Cómo puedo ayudarte hoy?</h2>
|
<p className={styles.subtitle}>
|
||||||
<div className={styles.cards}>
|
I am your personal intelligent assistant LobeChat, how can I help you now?
|
||||||
{suggestions.map((suggestion, index) => (
|
</p>
|
||||||
<div key={index} className={styles.card}>
|
<p className={styles.subtitle}>
|
||||||
<div className={styles.icon}>{suggestion.icon}</div>
|
If you need a more professional or customized assistant, you can click on{' '}
|
||||||
<div className={styles.cardContent}>
|
<span className={styles.highlight}>
|
||||||
<div className={styles.cardTitle}>{suggestion.title}</div>
|
<Sparkles size={12} />+
|
||||||
<div className={styles.cardDescription}>{suggestion.description}</div>
|
</span>{' '}
|
||||||
|
to create a custom assistant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Assistant Recommendations */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div className={styles.sectionTitle}>New Assistant Recommendations:</div>
|
||||||
|
<button className={styles.refreshButton}>
|
||||||
|
<RefreshCw size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.assistantsGrid}>
|
||||||
|
{assistants.map((assistant) => (
|
||||||
|
<div
|
||||||
|
key={assistant.id}
|
||||||
|
className={styles.assistantCard}
|
||||||
|
onClick={() => onAssistantSelect?.(assistant)}
|
||||||
|
>
|
||||||
|
<div className={styles.assistantHeader}>
|
||||||
|
<div className={styles.assistantIcon}>{assistant.icon}</div>
|
||||||
|
<div className={styles.assistantName}>{assistant.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.assistantDescription}>
|
||||||
|
{assistant.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Frequently Asked Questions */}
|
||||||
|
<div className={styles.questionsSection}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div className={styles.sectionTitle}>Frequently Asked Questions:</div>
|
||||||
|
<div className={styles.backToBottom}>
|
||||||
|
☰ Back to bottom
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
<div className={styles.questionsList}>
|
||||||
|
{frequentQuestions.map((question, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles.questionChip}
|
||||||
|
onClick={() => onQuestionSelect?.(question)}
|
||||||
|
>
|
||||||
|
{question}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lobehub/fluent-emoji": "^4.1.0",
|
"@lobehub/fluent-emoji": "^4.1.0",
|
||||||
"@lobehub/ui": "^4.38.0",
|
"@lobehub/ui": "^4.38.0",
|
||||||
|
"@prisma/client": "^7.4.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"antd": "^6.3.0",
|
"antd": "^6.3.0",
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
|
"prisma": "^7.4.0",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
|
|||||||
14
src/config/prisma.ts
Normal file
14
src/config/prisma.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||||
|
|
||||||
|
export const prisma =
|
||||||
|
globalForPrisma.prisma ||
|
||||||
|
new PrismaClient({
|
||||||
|
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user