From befb796b8bca1e69e11ef92ec0fab5f30085cd3a Mon Sep 17 00:00:00 2001 From: Iro Date: Mon, 1 Jun 2026 08:08:34 +0700 Subject: [PATCH 01/12] feat(frontend): add frontend --- apps/api/routes/chat.py | 30 ++- apps/api/routes/documents.py | 91 ++++++- apps/api/routes/memory.py | 38 +++ apps/api/routes/tools.py | 99 +++++++- deploy/docker-compose.yml | 15 ++ frontend/.gitignore | 9 + frontend/Dockerfile | 25 ++ frontend/README.md | 147 +++++++++++ frontend/index.html | 13 + frontend/package.json | 30 +++ frontend/src/App.vue | 125 ++++++++++ frontend/src/main.ts | 12 + frontend/src/router/index.ts | 55 +++++ frontend/src/services/api.ts | 28 +++ frontend/src/stores/auth.ts | 44 ++++ frontend/src/stores/chat.ts | 80 ++++++ frontend/src/style.scss | 104 ++++++++ frontend/src/views/ChatView.vue | 324 +++++++++++++++++++++++++ frontend/src/views/DocumentsView.vue | 348 +++++++++++++++++++++++++++ frontend/src/views/LoginView.vue | 199 +++++++++++++++ frontend/src/views/MemoryView.vue | 295 +++++++++++++++++++++++ frontend/src/views/ToolsView.vue | 301 +++++++++++++++++++++++ frontend/tsconfig.json | 33 +++ frontend/vite.config.ts | 21 ++ 24 files changed, 2453 insertions(+), 13 deletions(-) create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/stores/auth.ts create mode 100644 frontend/src/stores/chat.ts create mode 100644 frontend/src/style.scss create mode 100644 frontend/src/views/ChatView.vue create mode 100644 frontend/src/views/DocumentsView.vue create mode 100644 frontend/src/views/LoginView.vue create mode 100644 frontend/src/views/MemoryView.vue create mode 100644 frontend/src/views/ToolsView.vue create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/apps/api/routes/chat.py b/apps/api/routes/chat.py index b9caee8..da912a5 100644 --- a/apps/api/routes/chat.py +++ b/apps/api/routes/chat.py @@ -99,9 +99,27 @@ async def chat_stream_endpoint() -> BaseResponse: ) -@router.get("/threads/{thread_id}", response_model=ThreadResponse) -async def get_thread(thread_id: UUID, user: dict = Depends(get_current_user)) -> ThreadResponse: +@router.get("/chat/threads", response_model=dict) +async def list_threads(user: dict = Depends(get_current_user)): try: + threads = conversation_service.list_user_threads(UUID(str(user["user_id"]))) + except Exception as exc: + logger.error("Error listing threads: %s", exc) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to list threads", + ) from exc + + return { + "threads": threads, + "count": len(threads) + } + + +@router.get("/chat/threads/{thread_id}", response_model=dict) +async def get_thread(thread_id: UUID, user: dict = Depends(get_current_user)): + try: + thread = conversation_service.get_thread(UUID(str(user["user_id"])), thread_id) messages = conversation_service.list_thread_messages(UUID(str(user["user_id"])), thread_id) except KeyError as exc: raise HTTPException( @@ -109,7 +127,7 @@ async def get_thread(thread_id: UUID, user: dict = Depends(get_current_user)) -> detail="Thread not found", ) from exc - return ThreadResponse( - thread_id=thread_id, - messages=[ThreadMessageResponse(**message) for message in messages], - ) + return { + "thread": thread, + "messages": messages + } diff --git a/apps/api/routes/documents.py b/apps/api/routes/documents.py index 834bbe5..8221328 100644 --- a/apps/api/routes/documents.py +++ b/apps/api/routes/documents.py @@ -1,11 +1,92 @@ -from apps.api.fastapi_compat import APIRouter, File, HTTPException, UploadFile, status +import logging +from uuid import UUID +from apps.api.fastapi_compat import APIRouter, File, HTTPException, UploadFile, status, Depends +from apps.api.security import get_current_user from ..schemas.base import BaseResponse router = APIRouter() +logger = logging.getLogger(__name__) + +# In-memory document store (replace with database in production) +_documents_store = {} +_document_counter = 0 + @router.post("/documents/upload") -async def upload_document(file: UploadFile = File(...)) -> BaseResponse: - raise HTTPException( - status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="Document upload is not implemented in this beta", +async def upload_document( + file: UploadFile = File(...), + user: dict = Depends(get_current_user) +): + global _document_counter + + if not file.filename: + raise HTTPException(status_code=400, detail="No file selected") + + try: + contents = await file.read() + + _document_counter += 1 + doc_id = f"doc_{_document_counter}" + + document = { + "id": doc_id, + "filename": file.filename, + "size": len(contents), + "content_type": file.content_type or "text/plain", + "created_at": "2026-05-31T20:17:53Z", + "user_id": str(user.get("user_id", "")) + } + + _documents_store[doc_id] = { + **document, + "content": contents.decode('utf-8', errors='ignore') + } + + return {"document": document} + except Exception as e: + logger.error(f"Error uploading document: {e}") + raise HTTPException(status_code=500, detail="Failed to upload document") + + +@router.get("/documents") +async def list_documents(user: dict = Depends(get_current_user)): + user_id = str(user.get("user_id", "")) + documents = [ + {k: v for k, v in doc.items() if k != "content"} + for doc in _documents_store.values() + if doc.get("user_id") == user_id + ] + return {"documents": documents} + + +@router.get("/documents/{doc_id}/download") +async def download_document(doc_id: str, user: dict = Depends(get_current_user)): + if doc_id not in _documents_store: + raise HTTPException(status_code=404, detail="Document not found") + + doc = _documents_store[doc_id] + if doc.get("user_id") != str(user.get("user_id", "")): + raise HTTPException(status_code=403, detail="Unauthorized") + + from fastapi.responses import StreamingResponse + import io + + content = doc["content"].encode('utf-8') + return StreamingResponse( + iter([content]), + media_type=doc["content_type"], + headers={"Content-Disposition": f"attachment; filename={doc['filename']}"} ) + + +@router.delete("/documents/{doc_id}") +async def delete_document(doc_id: str, user: dict = Depends(get_current_user)): + if doc_id not in _documents_store: + raise HTTPException(status_code=404, detail="Document not found") + + doc = _documents_store[doc_id] + if doc.get("user_id") != str(user.get("user_id", "")): + raise HTTPException(status_code=403, detail="Unauthorized") + + del _documents_store[doc_id] + return {"message": "Document deleted"} diff --git a/apps/api/routes/memory.py b/apps/api/routes/memory.py index 8efa8c8..e7c7167 100644 --- a/apps/api/routes/memory.py +++ b/apps/api/routes/memory.py @@ -160,3 +160,41 @@ async def refresh_memories( updated_memory_ids=sorted(updated_ids, key=str), memories=serialized_memories, ) + + +@router.get("", response_model=dict) +async def get_memories_alias( + memory_type: Optional[str] = None, + limit: int = 50, + user: dict = Depends(get_current_user), +) -> dict: + """Alias for /memories endpoint for frontend compatibility""" + user_id = _require_user_id(user) + validated_memory_type = _validate_memory_type(memory_type) + bounded_limit = max(1, min(limit, 200)) + + try: + memories = memory_manager.retrieve_memories( + user_id=user_id, + thread_id=None, + memory_type=validated_memory_type, + key_pattern=None, + limit=bounded_limit, + ) + except Exception as exc: + raise HTTPException(status_code=503, detail="Memory storage unavailable") from exc + + serialized = [_serialize_memory(memory) for memory in memories] + return {"memories": serialized, "count": len(serialized)} + + +@router.delete("/{memory_id}") +async def delete_memory(memory_id: str, user: dict = Depends(get_current_user)) -> dict: + """Delete a memory by ID""" + user_id = _require_user_id(user) + + try: + memory_manager.delete_memory(UUID(memory_id), user_id) + return {"message": "Memory deleted"} + except Exception as exc: + raise HTTPException(status_code=500, detail="Failed to delete memory") from exc diff --git a/apps/api/routes/tools.py b/apps/api/routes/tools.py index 41aee63..4f21b61 100644 --- a/apps/api/routes/tools.py +++ b/apps/api/routes/tools.py @@ -1,15 +1,110 @@ -from apps.api.fastapi_compat import APIRouter, Depends -from typing import Dict, Any +from apps.api.fastapi_compat import APIRouter, Depends, HTTPException, status +from typing import Dict, Any, List from tools import run_tool from apps.api.security import get_current_user router = APIRouter(prefix="/tools") +# Available tools configuration +AVAILABLE_TOOLS: List[Dict[str, Any]] = [ + { + "id": "web_search", + "name": "Web Search", + "description": "Search the web for information", + "category": "information", + "enabled": True, + "version": "1.0", + "parameters": ["query", "max_results"] + }, + { + "id": "calculator", + "name": "Calculator", + "description": "Perform mathematical calculations", + "category": "utility", + "enabled": True, + "version": "1.0", + "parameters": ["expression"] + }, + { + "id": "file_read", + "name": "File Read", + "description": "Read content from a file", + "category": "file_operations", + "enabled": True, + "version": "1.0", + "parameters": ["file_path"] + }, + { + "id": "file_write", + "name": "File Write", + "description": "Write content to a file", + "category": "file_operations", + "enabled": True, + "version": "1.0", + "parameters": ["file_path", "content"] + }, + { + "id": "database_query", + "name": "Database Query", + "description": "Execute SQL queries", + "category": "database", + "enabled": False, + "version": "1.0", + "parameters": ["query"] + }, + { + "id": "api_call", + "name": "API Call", + "description": "Call external APIs", + "category": "integration", + "enabled": True, + "version": "1.0", + "parameters": ["url", "method", "headers", "body"] + } +] + +_tools_state = {tool["id"]: tool["enabled"] for tool in AVAILABLE_TOOLS} + + +@router.get("") +async def list_tools(user=Depends(get_current_user)) -> Dict[str, Any]: + """List all available tools""" + tools = [ + {**tool, "enabled": _tools_state.get(tool["id"], tool["enabled"])} + for tool in AVAILABLE_TOOLS + ] + return {"tools": tools} + + +@router.patch("/{tool_id}") +async def toggle_tool( + tool_id: str, + payload: Dict[str, bool], + user=Depends(get_current_user) +) -> Dict[str, Any]: + """Enable or disable a tool""" + if tool_id not in _tools_state: + raise HTTPException(status_code=404, detail="Tool not found") + + enabled = payload.get("enabled", _tools_state[tool_id]) + _tools_state[tool_id] = enabled + + return { + "tool_id": tool_id, + "enabled": enabled, + "message": f"Tool {'enabled' if enabled else 'disabled'}" + } + + @router.post("/run") async def run_tool_endpoint(payload: Dict[str, Any], user=Depends(get_current_user)) -> Dict[str, Any]: name = payload.get("name") args = payload.get("args", {}) if not name: return {"error": "Tool name is required"} + + if not _tools_state.get(name, False): + raise HTTPException(status_code=403, detail="Tool is disabled") + result = run_tool(name, args) return {"tool": name, "args": args, "result": result} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 21ca16b..43ad423 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,5 +1,20 @@ services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: mind_bus_frontend + ports: + - "5173:5173" + environment: + VITE_API_URL: http://localhost:8000 + depends_on: + - api + volumes: + - ./frontend/src:/app/src + - ./frontend/public:/app/public + api: image: ghcr.io/iro96/mind-bus:latest container_name: ai_agent_platform_api diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d6a9278 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +.vscode/ +.idea/ +*.log +.env +.env.local +.env.*.local +.DS_Store diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..1d41c2f --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +# Build stage +FROM node:18-alpine as builder + +WORKDIR /app + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + +# Production stage +FROM node:18-alpine + +WORKDIR /app + +RUN npm install -g serve + +COPY --from=builder /app/dist ./dist + +EXPOSE 5173 + +CMD ["serve", "-s", "dist", "-l", "5173"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..27a3b6f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,147 @@ +# Mind-Bus Frontend + +Vue 3 + TypeScript frontend for the Mind-Bus AI Agent Platform. + +## Features + +- **Modern Chat Interface** - Real-time chat with the AI agent +- **Memory Management** - View and manage long-term and episodic memories +- **Document Management** - Upload and manage documents for RAG +- **Tool Integration** - Control and monitor available tools +- **Dark Theme** - Beautiful dark mode optimized for extended use +- **Responsive Design** - Works on desktop and mobile + +## Getting Started + +### Prerequisites + +- Node.js 16+ +- npm or yarn + +### Installation + +```bash +cd frontend +npm install +``` + +### Development + +```bash +npm run dev +``` + +The frontend will be available at `http://localhost:5173` + +### Build + +```bash +npm run build +``` + +Production-ready files will be in the `dist` folder. + +## Project Structure + +``` +src/ +├── components/ # Reusable Vue components +├── views/ # Page components +├── router/ # Vue Router configuration +├── stores/ # Pinia state management +├── services/ # API and utility services +├── App.vue # Root component +├── main.ts # Application entry point +└── style.scss # Global styles +``` + +## API Integration + +The frontend communicates with the backend API at `/api`. The development server includes a proxy that forwards requests to `http://localhost:8000`. + +### Authentication + +The app uses JWT token-based authentication: +1. User logs in at `/login` +2. Token is stored in localStorage +3. Token is sent in the `Authorization: Bearer {token}` header for all requests + +### State Management + +Pinia is used for state management with two main stores: + +- **AuthStore** - Handles user authentication and authorization +- **ChatStore** - Manages chat messages, threads, and conversation state + +## Styling + +The frontend uses SCSS for styling with: +- CSS Grid and Flexbox for layouts +- CSS custom properties for theming +- Utility classes for common patterns +- Responsive breakpoints for mobile support + +## Components + +### Views +- **LoginView** - User authentication +- **ChatView** - Main chat interface with thread management +- **MemoryView** - Memory visualization and management +- **DocumentsView** - Document upload and management +- **ToolsView** - Tool integration control panel + +### Stores +- **useAuthStore** - Authentication state and methods +- **useChatStore** - Chat messages and thread management + +## Configuration + +### Vite Config +Configured in `vite.config.ts`: +- Vue 3 plugin support +- Path alias resolution (@) +- Development API proxy + +### TypeScript +Strict mode enabled with Vue support in `tsconfig.json` + +## Browser Support + +- Chrome/Edge 90+ +- Firefox 88+ +- Safari 14+ + +## Development Tips + +### Adding New Pages +1. Create a new component in `src/views/` +2. Add route in `src/router/index.ts` +3. Link in the navigation bar + +### Adding New Stores +1. Create new store in `src/stores/` +2. Use `defineStore('name', () => { ... })` +3. Import and use with `useStore()` + +### API Calls +Import the api service and use it: +```typescript +import axios from '@/services/api' + +const response = await axios.get('/endpoint') +``` + +## Troubleshooting + +### CORS Issues +Make sure the development server proxy is configured correctly in `vite.config.ts` + +### Authentication Failures +Check that the backend is running on port 8000 and the token is being sent correctly + +### Build Errors +Run `npm install` to ensure all dependencies are installed + +## License + +MIT diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9c3b0a3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Mind-Bus - AI Agent Platform + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0af73d4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "mind-bus-frontend", + "version": "1.0.0", + "description": "Frontend for Mind-Bus AI Agent Platform", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "pinia": "^2.1.4", + "axios": "^1.5.0", + "date-fns": "^2.30.0", + "marked": "^9.0.0", + "highlight.js": "^11.8.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.3.4", + "@vue/tsconfig": "^0.4.0", + "typescript": "^5.2.2", + "vite": "^4.4.9", + "vue-tsc": "^1.8.13", + "sass": "^1.66.1", + "sass-loader": "^13.3.2" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..ff6d136 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..5166750 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import './style.scss' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..5e0ecf8 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,55 @@ +import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' +import { useAuthStore } from '@/stores/auth' +import LoginView from '@/views/LoginView.vue' +import ChatView from '@/views/ChatView.vue' +import MemoryView from '@/views/MemoryView.vue' +import DocumentsView from '@/views/DocumentsView.vue' +import ToolsView from '@/views/ToolsView.vue' + +const routes: Array = [ + { + path: '/login', + component: LoginView, + meta: { requiresAuth: false } + }, + { + path: '/', + component: ChatView, + meta: { requiresAuth: true } + }, + { + path: '/memory', + component: MemoryView, + meta: { requiresAuth: true } + }, + { + path: '/documents', + component: DocumentsView, + meta: { requiresAuth: true } + }, + { + path: '/tools', + component: ToolsView, + meta: { requiresAuth: true } + } +] + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}) + +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + const requiresAuth = to.meta.requiresAuth !== false + + if (requiresAuth && !authStore.isAuthenticated) { + next('/login') + } else if (to.path === '/login' && authStore.isAuthenticated) { + next('/') + } else { + next() + } +}) + +export default router diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..7aa7a1b --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,28 @@ +import axios from 'axios' +import { useAuthStore } from '@/stores/auth' + +const api = axios.create({ + baseURL: '/api' +}) + +api.interceptors.request.use((config) => { + const authStore = useAuthStore() + if (authStore.token) { + config.headers.Authorization = `Bearer ${authStore.token}` + } + return config +}) + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + const authStore = useAuthStore() + authStore.logout() + window.location.href = '/login' + } + return Promise.reject(error) + } +) + +export default api diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..4e8344c --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,44 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import axios from '@/services/api' + +interface AuthUser { + username: string + user_id: string + roles: string[] +} + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('token')) + const user = ref(null) + + const isAuthenticated = computed(() => !!token.value) + + const login = async (username: string, password: string) => { + const response = await axios.post('/auth/token', { username, password }) + token.value = response.data.access_token + localStorage.setItem('token', response.data.access_token) + + // Decode token to get user info (basic JWT parsing) + const payload = JSON.parse(atob(response.data.access_token.split('.')[1])) + user.value = { + username: payload.sub, + user_id: payload.user_id, + roles: payload.roles + } + } + + const logout = () => { + token.value = null + user.value = null + localStorage.removeItem('token') + } + + return { + token, + user, + isAuthenticated, + login, + logout + } +}) diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts new file mode 100644 index 0000000..7baa165 --- /dev/null +++ b/frontend/src/stores/chat.ts @@ -0,0 +1,80 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import axios from '@/services/api' + +interface Thread { + id: string + title: string + created_at: string + updated_at: string +} + +interface Message { + id: string + thread_id: string + role: string + content: string + created_at: string +} + +export const useChatStore = defineStore('chat', () => { + const threads = ref([]) + const currentThread = ref(null) + const messages = ref([]) + const loading = ref(false) + const error = ref(null) + + const fetchThreads = async () => { + loading.value = true + try { + const response = await axios.get('/chat/threads') + threads.value = response.data.threads || [] + } catch (err) { + error.value = 'Failed to fetch threads' + } finally { + loading.value = false + } + } + + const fetchMessages = async (threadId: string) => { + loading.value = true + try { + const response = await axios.get(`/chat/threads/${threadId}`) + currentThread.value = response.data.thread + messages.value = response.data.messages || [] + } catch (err) { + error.value = 'Failed to fetch messages' + } finally { + loading.value = false + } + } + + const sendMessage = async (message: string, threadId?: string) => { + loading.value = true + try { + const response = await axios.post('/chat', { + message, + thread_id: threadId + }) + + currentThread.value = response.data.thread + messages.value = response.data.messages || [] + await fetchThreads() + } catch (err) { + error.value = 'Failed to send message' + } finally { + loading.value = false + } + } + + return { + threads, + currentThread, + messages, + loading, + error, + fetchThreads, + fetchMessages, + sendMessage + } +}) diff --git a/frontend/src/style.scss b/frontend/src/style.scss new file mode 100644 index 0000000..814b74e --- /dev/null +++ b/frontend/src/style.scss @@ -0,0 +1,104 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background-color: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +#app { + width: 100%; + height: 100vh; +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #1e293b; +} + +::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 4px; + + &:hover { + background: #64748b; + } +} + +/* Firefox Scrollbar */ +* { + scrollbar-color: #475569 #1e293b; + scrollbar-width: thin; +} + +/* Selection */ +::selection { + background: #3b82f6; + color: white; +} + +/* Focus Visible */ +:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideInUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideInDown { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Utility Classes */ +.fade-in { + animation: fadeIn 0.3s ease-out; +} + +.slide-in-up { + animation: slideInUp 0.3s ease-out; +} + +.slide-in-down { + animation: slideInDown 0.3s ease-out; +} + diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue new file mode 100644 index 0000000..60d1f72 --- /dev/null +++ b/frontend/src/views/ChatView.vue @@ -0,0 +1,324 @@ +