diff --git a/client/src/App.vue b/client/src/App.vue index e69de29..a156508 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/client/src/components/CenterPanel/CenterPanel.vue b/client/src/components/CenterPanel/CenterPanel.vue index e69de29..314c584 100644 --- a/client/src/components/CenterPanel/CenterPanel.vue +++ b/client/src/components/CenterPanel/CenterPanel.vue @@ -0,0 +1,17 @@ + + + Center Panel - Chat Interface + + + + + + diff --git a/client/src/components/LeftPanel/LeftPanel.vue b/client/src/components/LeftPanel/LeftPanel.vue index e69de29..f4504c2 100644 --- a/client/src/components/LeftPanel/LeftPanel.vue +++ b/client/src/components/LeftPanel/LeftPanel.vue @@ -0,0 +1,18 @@ + + + Left Panel - Character List & Chat History + + + + + + diff --git a/client/src/components/RightPanel/RightPanel.vue b/client/src/components/RightPanel/RightPanel.vue index e69de29..6f09629 100644 --- a/client/src/components/RightPanel/RightPanel.vue +++ b/client/src/components/RightPanel/RightPanel.vue @@ -0,0 +1,18 @@ + + + Right Panel - Character Detail & Workflow Editor + + + + + + diff --git a/client/src/components/TopBar/TopBar.vue b/client/src/components/TopBar/TopBar.vue index e69de29..ea28e66 100644 --- a/client/src/components/TopBar/TopBar.vue +++ b/client/src/components/TopBar/TopBar.vue @@ -0,0 +1,20 @@ + + + SillyTavern Repalice + + + + + + diff --git a/client/src/layouts/MainLayout/MainLayout.vue b/client/src/layouts/MainLayout/MainLayout.vue index e69de29..1ac85c7 100644 --- a/client/src/layouts/MainLayout/MainLayout.vue +++ b/client/src/layouts/MainLayout/MainLayout.vue @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/client/src/main.ts b/client/src/main.ts index e69de29..572ea83 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -0,0 +1,14 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router'; +import './styles/reset.css'; +import './styles/variables.css'; + +const app = createApp(App); +const pinia = createPinia(); + +app.use(pinia); +app.use(router); + +app.mount('#app'); diff --git a/client/src/router/index.ts b/client/src/router/index.ts index e69de29..c76aec0 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -0,0 +1,35 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import MainLayout from '@/layouts/MainLayout/MainLayout.vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + component: MainLayout, + children: [ + { + path: '', + redirect: '/chat', + }, + { + path: 'chat', + name: 'Chat', + component: () => import('@/components/CenterPanel/CenterPanel.vue'), + }, + { + path: 'characters', + name: 'Characters', + component: () => import('@/components/LeftPanel/LeftPanel.vue'), + }, + { + path: 'character/:id', + name: 'CharacterDetail', + component: () => import('@/components/RightPanel/RightPanel.vue'), + }, + ], + }, + ], +}); + +export default router; diff --git a/client/src/stores/useAppStore.ts b/client/src/stores/useAppStore.ts index e69de29..f637f0f 100644 --- a/client/src/stores/useAppStore.ts +++ b/client/src/stores/useAppStore.ts @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export const useAppStore = defineStore('app', () => { + // State + const theme = ref<'light' | 'dark'>('dark'); + const sidebarCollapsed = ref(false); + + // Actions + function toggleTheme() { + theme.value = theme.value === 'light' ? 'dark' : 'light'; + } + + function toggleSidebar() { + sidebarCollapsed.value = !sidebarCollapsed.value; + } + + return { + theme, + sidebarCollapsed, + toggleTheme, + toggleSidebar, + }; +}); diff --git a/client/src/styles/reset.css b/client/src/styles/reset.css index e69de29..af3d288 100644 --- a/client/src/styles/reset.css +++ b/client/src/styles/reset.css @@ -0,0 +1,49 @@ +/* CSS Reset */ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +ul, +ol { + list-style: none; +} + +a { + text-decoration: none; + color: inherit; +} + +button { + border: none; + background: none; + cursor: pointer; + font: inherit; + color: inherit; +} + +input, +textarea, +select { + font: inherit; + color: inherit; +} + +img, +video { + max-width: 100%; + height: auto; + display: block; +} diff --git a/client/src/styles/variables.css b/client/src/styles/variables.css index e69de29..030efe3 100644 --- a/client/src/styles/variables.css +++ b/client/src/styles/variables.css @@ -0,0 +1,72 @@ +/* CSS Variables */ +:root { + /* Colors - Dark Theme (Default) */ + --color-bg-primary: #1a1a1a; + --color-bg-secondary: #252525; + --color-bg-tertiary: #303030; + + --color-text-primary: #e0e0e0; + --color-text-secondary: #b0b0b0; + --color-text-muted: #808080; + + --color-border: #404040; + --color-border-light: #505050; + + --color-accent: #6c63ff; + --color-accent-hover: #7b73ff; + --color-accent-active: #5a52d5; + + --color-success: #4caf50; + --color-warning: #ff9800; + --color-error: #f44336; + + /* Spacing */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5); + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + --transition-slow: 350ms ease; + + /* Z-index layers */ + --z-dropdown: 1000; + --z-sticky: 1020; + --z-fixed: 1030; + --z-modal-backdrop: 1040; + --z-modal: 1050; + --z-popover: 1060; + --z-tooltip: 1070; +} + +/* Light Theme */ +[data-theme='light'] { + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-bg-tertiary: #e8e8e8; + + --color-text-primary: #212121; + --color-text-secondary: #616161; + --color-text-muted: #9e9e9e; + + --color-border: #e0e0e0; + --color-border-light: #d0d0d0; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.15); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.2); +} diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..f07b54a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + # Backend Service + backend: + build: + context: . + dockerfile: docker/backend.Dockerfile + container_name: sillytavern-repalice-backend + ports: + - "3000:3000" + volumes: + - ./data:/app/data + environment: + - NODE_ENV=production + - PORT=3000 + - DATA_DIR=/app/data + - FRONTEND_URL=http://localhost:5173 + env_file: + - .env + networks: + - app-network + restart: unless-stopped + + # Frontend Service + frontend: + build: + context: . + dockerfile: docker/frontend.Dockerfile + container_name: sillytavern-repalice-frontend + ports: + - "5173:5173" + environment: + - VITE_API_URL=http://localhost:3000/api + depends_on: + - backend + networks: + - app-network + restart: unless-stopped + +networks: + app-network: + driver: bridge diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile index e69de29..c888263 100644 --- a/docker/backend.Dockerfile +++ b/docker/backend.Dockerfile @@ -0,0 +1,40 @@ +# Backend Dockerfile - NestJS +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY server/package*.json ./server/ +COPY shared/package*.json ./shared/ + +# Install dependencies +RUN cd server && npm ci + +# Copy source code +COPY server/src ./server/src +COPY server/tsconfig.json ./server/ +COPY server/nest-cli.json ./server/ +COPY shared ./shared + +# Build +RUN cd server && npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files and install production dependencies +COPY server/package*.json ./ +RUN npm ci --only=production + +# Copy built application +COPY --from=builder /app/server/dist ./dist +COPY shared ./shared + +# Create data directory +RUN mkdir -p /app/data + +EXPOSE 3000 + +CMD ["node", "dist/main.js"] diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile index e69de29..c6b713c 100644 --- a/docker/frontend.Dockerfile +++ b/docker/frontend.Dockerfile @@ -0,0 +1,31 @@ +# Frontend Dockerfile - Vue3 + Vite +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY client/package*.json ./client/ +COPY shared/package*.json ./shared/ + +# Install dependencies +RUN cd client && npm ci + +# Copy source code +COPY client ./client +COPY shared ./shared + +# Build +RUN cd client && npm run build + +# Production stage - serve with nginx +FROM nginx:alpine + +# Copy custom nginx config +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +# Copy built files +COPY --from=builder /app/client/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index e69de29..f92bccf 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # Serve static files + location / { + try_files $uri $uri/ /index.html; + } + + # API proxy to backend + location /api/ { + proxy_pass http://backend:3000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index e69de29..c711ee2 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { PersistenceModule } from './persistence/persistence.module'; +import { ChatModule } from './modules/chat/chat.module'; +import { CharacterModule } from './modules/character/character.module'; +import { LLMModule } from './modules/llm/llm.module'; +import { ImportExportModule } from './modules/import-export/import-export.module'; +import { WorkflowModule } from './modules/workflow/workflow.module'; + +@Module({ + imports: [ + // 配置模块 + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env.local', '.env'], + }), + + // 持久化层 + PersistenceModule, + + // 业务模块 + ChatModule, + CharacterModule, + LLMModule, + ImportExportModule, + WorkflowModule, + ], +}) +export class AppModule {} diff --git a/server/src/main.ts b/server/src/main.ts index e69de29..5e88ee0 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -0,0 +1,41 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; +import { HttpExceptionFilter } from './core/filters/http-exception.filter'; +import { LoggingInterceptor, ResponseInterceptor } from './core/interceptors'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // 全局前缀 + app.setGlobalPrefix('api'); + + // CORS 配置 + app.enableCors({ + origin: process.env.FRONTEND_URL || 'http://localhost:5173', + credentials: true, + }); + + // 全局验证管道 + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + // 全局异常过滤器 + app.useGlobalFilters(new HttpExceptionFilter()); + + // 全局拦截器 + app.useGlobalInterceptors(new LoggingInterceptor()); + app.useGlobalInterceptors(new ResponseInterceptor()); + + const port = process.env.PORT || 3000; + await app.listen(port); + + console.log(`🚀 Server is running on http://localhost:${port}`); +} + +bootstrap(); diff --git a/server/src/persistence/persistence.module.ts b/server/src/persistence/persistence.module.ts index e69de29..c13e191 100644 --- a/server/src/persistence/persistence.module.ts +++ b/server/src/persistence/persistence.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { FileSystemRepository } from './file-system/file-system.repository'; + +@Global() +@Module({ + providers: [FileSystemRepository], + exports: [FileSystemRepository], +}) +export class PersistenceModule {}
Center Panel - Chat Interface
Left Panel - Character List & Chat History
Right Panel - Character Detail & Workflow Editor