diff --git a/README.md b/README.md index 711ab5b..dc2c277 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,122 @@ -# AIUI - Web Chat Interface +# AIUI Chat -A modern, responsive web interface for AI chat with OpenAI-compatible API support and streaming responses. +AIUI Chat — это современный веб-интерфейс для общения с LLM (большими языковыми моделями), вдохновленный Open WebUI. Поддерживает любой OpenAI-compatible API (OpenAI, Ollama, LM Studio, vLLM и другие). -## Features +## Особенности -- 💬 Real-time streaming chat responses -- 🌙 Beautiful dark theme -- 📱 Fully responsive (desktop + mobile) -- 🔧 Customizable API settings (base URL, model, API key) -- 📜 Multiple conversation support -- ✨ Markdown rendering with syntax highlighting -- ⚡ Built with React + TypeScript + Tailwind CSS + Vite +- Чат с поддержкой **streaming** ответов +- Управление **историей** диалогов +- Настройки API, модели, temperature, max tokens +- Загрузка файлов в чат +- **Базы знаний** с ChromaDB — загружайте документы и получайте ответы с учетом контекста +- Адаптивный дизайн (десктоп + мобильные) +- Темная тема в стиле modern tech -## Project Structure +## Структура проекта ``` AIUI/ -├── frontend/ # React frontend +├── frontend/ # React + TypeScript + Tailwind CSS │ ├── src/ -│ │ ├── components/ # React components -│ │ ├── hooks/ # Custom hooks -│ │ ├── services/ # API services -│ │ ├── stores/ # State management (Zustand) -│ │ ├── types/ # TypeScript types -│ │ └── utils/ # Utilities +│ │ ├── components/ # UI компоненты +│ │ │ ├── chat/ # Компоненты чата +│ │ │ ├── layout/ # Layout компоненты +│ │ │ └── knowledge/ # Компоненты баз знаний +│ │ ├── services/ # API сервисы +│ │ ├── stores/ # Zustand stores +│ │ ├── types/ # TypeScript типы +│ │ └── utils/ # Утилиты │ └── package.json -├── backend/ # Express proxy server +├── backend/ # Express.js proxy сервер │ └── src/ -│ └── index.js -├── package.json # Root package.json with workspace scripts -└── README.md +│ └── index.js # Основной сервер +└── package.json # Root package.json ``` -## Quick Start +## Быстрый старт -### Prerequisites -- Node.js 18+ -- npm - -### 1. Install Dependencies +### 1. Установка зависимостей ```bash -# Install root dependencies npm install - -# Install backend dependencies -cd backend && npm install && cd .. - -# Install frontend dependencies -cd frontend && npm install && cd .. ``` -### 2. Start the Application +Это установит зависимости для root, frontend и backend. + +### 2. Настройка API + +Запустите frontend и backend, затем откройте настройки (иконка шестеренки) и укажите: + +- **API Base URL** — например `https://api.openai.com/v1` или `http://localhost:11434/v1` (для Ollama) +- **API Key** — ваш ключ +- **Model** — например `gpt-4`, `gpt-3.5-turbo`, `llama2` и т.д. +- **Temperature** и **Max Tokens** — по желанию +- **System Prompt** — системный промпт +- **Embedding Model** — для баз знаний (например `text-embedding-3-small`) +- **ChromaDB Directory** — путь к хранилищу ChromaDB + +### 3. Запуск ```bash -# Start both frontend and backend -npm run dev +# Запуск backend (порт 3001) +npm run dev:backend + +# В другом терминале — запуск frontend (порт 3000) +npm run dev:frontend ``` -Or start them separately: +Frontend запустится на `http://localhost:3000` и будет проксировать API-запросы на backend. + +### Production сборка ```bash -# Terminal 1 - Backend (port 3001) -npm run backend +# Сборка frontend +npm run build:frontend -# Terminal 2 - Frontend (port 3000) -npm run frontend +# Запуск backend (раздает собранный frontend) +npm run start:backend ``` -### 3. Open in Browser +## Базы знаний (Knowledge Bases) -Navigate to http://localhost:3000 +Базы знаний позволяют загружать документы и использовать их как контекст для ответов LLM. -## Usage +### Как использовать: -1. **Configure API**: Click the settings icon (⚙️) in the top right to set your: - - API Base URL (e.g., `https://api.openai.com/v1`) - - API Key - - Model name (e.g., `gpt-4o-mini`, `gpt-4o`) +1. Откройте панель **Knowledge Bases** (иконка базы данных в сайдбаре) +2. Создайте новую базу знаний +3. Загрузите документы (`.txt`, `.md`, `.pdf`, `.docx`) +4. В сайдбаре выберите базу знаний для текущего диалога +5. Задавайте вопросы — LLM будет использовать контекст из документов -2. **Start Chatting**: Click "New Chat" and send a message! +### Технические детали: -3. **Mobile**: The sidebar is collapsible - use the ☰ button to toggle it. +- Документы разбиваются на **чанки** по ~500 символов с перекрытием +- Для каждого чанка генерируется **embedding** через API +- Чанки хранятся в **ChromaDB** (векторная база данных) +- При отправке сообщения запрос эмбеддится и ищутся похожие чанки через semantic search +- Найденный контекст добавляется к системному промпту -## Development +## API Endpoints -### Build for Production +Backend проксирует запросы к OpenAI-compatible API: -```bash -npm run build -``` +| Endpoint | Method | Описание | +|----------|--------|----------| +| `/api/v1/chat/completions` | POST | Streaming чат | +| `/api/v1/models` | POST | Список моделей | +| `/api/files/upload` | POST | Загрузка файлов | +| `/api/files` | GET | Список файлов | +| `/api/files/:id` | DELETE | Удаление файла | +| `/api/knowledge` | GET | Список баз знаний | +| `/api/knowledge` | POST | Создать базу знаний | +| `/api/knowledge/:id` | DELETE | Удалить базу знаний | +| `/api/knowledge/:id/files` | POST | Добавить файл в базу | +| `/api/knowledge/:id/files/:fileId` | DELETE | Удалить файл из базы | +| `/api/knowledge/:id/query` | POST | Поиск по базе знаний | -### Build Frontend Only +## Технологии -```bash -cd frontend && npm run build -``` - -## API Configuration - -The interface supports any OpenAI-compatible API: - -| Provider | Base URL | Example Models | -|----------|----------|----------------| -| OpenAI | `https://api.openai.com/v1` | gpt-4o-mini, gpt-4o | -| OpenRouter | `https://openrouter.ai/api/v1` | varies | -| Local (Ollama) | `http://localhost:11434/v1` | llama2, mistral | -| Custom | Your endpoint | Your models | - -## Tech Stack - -- **Frontend**: React 18, TypeScript, Tailwind CSS, Vite, Zustand, Lucide Icons -- **Backend**: Express, CORS, node-fetch -- **Build**: TypeScript Compiler, Vite - -## License - -MIT +- **Frontend**: React 18, TypeScript, Tailwind CSS, Zustand, Lucide icons +- **Backend**: Node.js, Express.js, ChromaDB, Multer +- **Build**: Vite diff --git a/backend/package-lock.json b/backend/package-lock.json index 21b4b10..0ae78a5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,15 +1,28 @@ { "name": "aiui-backend", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aiui-backend", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { + "chromadb": "^1.8.1", "cors": "^2.8.5", - "express": "^4.18.2" + "express": "^4.21.2", + "mammoth": "^1.9.0", + "multer": "^1.4.5-lts.2", + "pdf-parse": "^1.1.1" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, "node_modules/accepts": { @@ -25,12 +38,77 @@ "node": ">= 0.6" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -70,6 +148,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -108,6 +203,90 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chromadb": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/chromadb/-/chromadb-1.10.5.tgz", + "integrity": "sha512-+IeTjjf44pKUY3vp1BacwO2tFAPcWCd64zxPZZm98dVj/kbSBeaHKB2D6eX7iRLHS1PTVASuqoR6mAJ+nrsTBg==", + "license": "Apache-2.0", + "dependencies": { + "cliui": "^8.0.1", + "isomorphic-fetch": "^3.0.0" + }, + "engines": { + "node": ">=14.17.0" + }, + "peerDependencies": { + "@google/generative-ai": "^0.1.1", + "cohere-ai": "^5.0.0 || ^6.0.0 || ^7.0.0", + "ollama": "^0.5.0", + "openai": "^3.0.0 || ^4.0.0", + "voyageai": "^0.0.3-1" + }, + "peerDependenciesMeta": { + "@google/generative-ai": { + "optional": true + }, + "cohere-ai": { + "optional": true + }, + "ollama": { + "optional": true + }, + "openai": { + "optional": true + }, + "voyageai": { + "optional": true + } + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -144,6 +323,12 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -189,6 +374,21 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -209,6 +409,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -459,6 +665,12 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -474,6 +686,87 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -543,12 +836,52 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -558,6 +891,32 @@ "node": ">= 0.6" } }, + "node_modules/node-ensure": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -591,6 +950,18 @@ "node": ">= 0.8" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -600,12 +971,43 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.4.tgz", + "integrity": "sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==", + "license": "MIT", + "dependencies": { + "node-ensure": "^0.0.0" + }, + "engines": { + "node": ">=6.8.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -658,6 +1060,27 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -729,6 +1152,12 @@ "node": ">= 0.8.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -807,6 +1236,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -816,6 +1251,55 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -825,6 +1309,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -838,6 +1328,18 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -847,6 +1349,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -864,6 +1372,63 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } } } } diff --git a/backend/package.json b/backend/package.json index 8214eed..456da4b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,14 +1,18 @@ { "name": "aiui-backend", - "version": "0.1.0", - "private": true, - "type": "module", + "version": "1.0.0", + "description": "AIUI Chat Backend", + "main": "src/index.js", "scripts": { - "dev": "node src/index.js", + "dev": "node --watch src/index.js", "start": "node src/index.js" }, "dependencies": { + "chromadb": "^1.8.1", "cors": "^2.8.5", - "express": "^4.18.2" + "express": "^4.21.2", + "mammoth": "^1.9.0", + "multer": "^1.4.5-lts.2", + "pdf-parse": "^1.1.1" } } diff --git a/backend/src/index.js b/backend/src/index.js index 43828c1..d76d043 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,61 +1,59 @@ -import express from 'express'; -import cors from 'cors'; +const express = require('express'); +const cors = require('cors'); +const { ChromaClient } = require('chromadb'); +const multer = require('multer'); +const fs = require('fs'); +const path = require('path'); const app = express(); const PORT = process.env.PORT || 3001; +// Middleware app.use(cors()); app.use(express.json()); -// Health check -app.get('/api/health', (_req, res) => { - res.json({ status: 'ok' }); +// File upload config +const uploadDir = path.join(__dirname, '../../data/uploads'); +if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => cb(null, uploadDir), + filename: (req, file, cb) => { + const uniqueName = Date.now() + '-' + Math.random().toString(36).substring(2, 8); + cb(null, uniqueName + path.extname(file.originalname)); + }, }); -// Proxy: list models -app.post('/api/v1/models', async (req, res) => { - try { - const { apiUrl, apiKey } = req.body; +const upload = multer({ storage }); - if (!apiUrl || !apiKey) { - return res.status(400).json({ error: { message: 'apiUrl and apiKey are required' } }); - } +// In-memory storage for knowledge bases metadata +const knowledgeBases = new Map(); +const knowledgeFiles = new Map(); +const uploadedFiles = new Map(); - const response = await fetch(`${apiUrl}/models`, { - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - }); +// Global ChromaDB client (recreated per request when path changes) +function getChromaClient(persistDir = './data/chromadb') { + return new ChromaClient({ + path: `file://${path.resolve(persistDir)}`, + }); +} - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return res.status(response.status).json(errorData); - } +// ===== OPENAI PROXY ===== - const data = await response.json(); - const models = data.data?.map((m) => m.id) || []; - res.json({ models }); - } catch (error) { - console.error('Models fetch error:', error); - res.status(500).json({ error: { message: error.message || 'Internal server error' } }); - } -}); - -// Proxy: chat completions with streaming app.post('/api/v1/chat/completions', async (req, res) => { try { const { apiUrl, apiKey, model, temperature, max_tokens, messages, stream } = req.body; if (!apiUrl || !apiKey || !model) { - return res.status(400).json({ error: { message: 'apiUrl, apiKey, and model are required' } }); + return res.status(400).json({ error: { message: 'Missing API configuration' } }); } - const response = await fetch(`${apiUrl}/chat/completions`, { + const url = `${apiUrl.replace(/\/$/, '')}/chat/completions`; + const response = await fetch(url, { method: 'POST', headers: { - 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model, @@ -67,42 +65,369 @@ app.post('/api/v1/chat/completions', async (req, res) => { }); if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return res.status(response.status).json(errorData); + const errorData = await response.text(); + return res.status(response.status).json({ + error: { message: `Proxy error: ${response.statusText}. ${errorData}` }, + }); } - // Stream the response back to client - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + // Stream the response + res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const reader = response.body.getReader(); + const decoder = new TextDecoder(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - res.write(Buffer.from(value)); - // Flush to ensure real-time streaming - if (res.flush) res.flush(); - } - } catch (err) { - console.error('Stream read error:', err); - } finally { - reader.releaseLock(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + res.write(decoder.decode(value, { stream: true })); } - res.end(); } catch (error) { - console.error('Chat completion error:', error); - if (!res.headersSent) { - res.status(500).json({ error: { message: error.message || 'Internal server error' } }); - } else { - res.end(); - } + console.error('Proxy error:', error); + res.status(500).json({ + error: { message: error.message || 'Internal server error' }, + }); } }); +app.post('/api/v1/models', async (req, res) => { + try { + const { apiUrl, apiKey } = req.body; + if (!apiUrl || !apiKey) { + return res.status(400).json({ error: { message: 'Missing API configuration' } }); + } + + const url = `${apiUrl.replace(/\/$/, '')}/models`; + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + }, + }); + + if (!response.ok) { + return res.status(response.status).json({ + error: { message: `Failed to fetch models: ${response.statusText}` }, + }); + } + + const data = await response.json(); + const models = data.data?.map((m) => m.id) || []; + res.json({ models }); + } catch (error) { + console.error('Models error:', error); + res.status(500).json({ error: { message: error.message } }); + } +}); + +// ===== FILE UPLOADS ===== + +app.post('/api/files/upload', upload.array('files', 5), (req, res) => { + try { + const files = req.files || []; + const result = files.map((f) => { + const fileInfo = { + id: f.filename, + originalName: f.originalname, + size: f.size, + mimeType: f.mimetype, + uploadedAt: Date.now(), + }; + uploadedFiles.set(f.filename, fileInfo); + return fileInfo; + }); + res.json({ files: result }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/files', (req, res) => { + const files = Array.from(uploadedFiles.values()); + res.json({ files }); +}); + +app.delete('/api/files/:id', (req, res) => { + const { id } = req.params; + const fileInfo = uploadedFiles.get(id); + if (fileInfo) { + const filePath = path.join(uploadDir, id); + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + uploadedFiles.delete(id); + } + res.json({ success: true }); +}); + +// ===== KNOWLEDGE BASES ===== + +function chunkText(text, chunkSize = 500, overlap = 50) { + const chunks = []; + let start = 0; + while (start < text.length) { + const end = Math.min(start + chunkSize, text.length); + chunks.push(text.slice(start, end)); + start += chunkSize - overlap; + if (start >= text.length) break; + // Don't create tiny chunks at the end + if (text.length - start < chunkSize * 0.3) { + chunks[chunks.length - 1] = text.slice(chunks.length > 0 ? start - (chunkSize - overlap) : 0); + break; + } + } + return chunks; +} + +async function getEmbeddings(texts, apiUrl, apiKey, model) { + const url = `${apiUrl.replace(/\/$/, '')}/embeddings`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + }, + body: JSON.stringify({ model, input: texts }), + }); + + if (!response.ok) { + const err = await response.text(); + throw new Error(`Embedding failed: ${response.status} ${err}`); + } + + const data = await response.json(); + return data.data.map((d) => d.embedding); +} + +app.get('/api/knowledge', async (req, res) => { + const { chromaDir } = req.query; + try { + const client = getChromaClient(chromaDir || './data/chromadb'); + // Get all collections to list knowledge bases + const collections = await client.listCollections(); + const bases = []; + + for (const colName of collections) { + const kb = knowledgeBases.get(colName); + if (kb) { + const files = knowledgeFiles.get(colName) || []; + const totalChunks = files.reduce((s, f) => s + (f.chunkCount || 0), 0); + bases.push({ + ...kb, + documentCount: totalChunks, + files: files, + }); + } + } + + res.json({ bases }); + } catch (error) { + console.error('List knowledge bases error:', error); + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/knowledge', async (req, res) => { + const { name, description, chromaDir } = req.body; + if (!name || !name.trim()) { + return res.status(400).json({ error: 'Name is required' }); + } + + const id = name.trim().toLowerCase().replace(/[^a-z0-9]/g, '_') + '_' + Date.now(); + + try { + const client = getChromaClient(chromaDir || './data/chromadb'); + await client.getOrCreateCollection({ + name: id, + metadata: { name: name.trim(), description: description || '', createdAt: Date.now() }, + }); + + const kb = { + id, + name: name.trim(), + description: description || '', + documentCount: 0, + files: [], + }; + + knowledgeBases.set(id, kb); + knowledgeFiles.set(id, []); + res.json(kb); + } catch (error) { + console.error('Create knowledge base error:', error); + res.status(500).json({ error: error.message }); + } +}); + +app.delete('/api/knowledge/:id', async (req, res) => { + const { id } = req.params; + const { chromaDir } = req.query; + + try { + const client = getChromaClient(chromaDir || './data/chromadb'); + await client.deleteCollection({ name: id }); + knowledgeBases.delete(id); + knowledgeFiles.delete(id); + res.json({ success: true }); + } catch (error) { + console.error('Delete knowledge base error:', error); + // Even if Chroma throws, clean up our state + knowledgeBases.delete(id); + knowledgeFiles.delete(id); + res.json({ success: true }); + } +}); + +app.post('/api/knowledge/:id/files', upload.single('file'), async (req, res) => { + const { id } = req.params; + const { apiUrl, apiKey, embeddingModel } = req.body; + const file = req.file; + + if (!file) return res.status(400).json({ error: 'No file uploaded' }); + if (!apiUrl || !apiKey) { + return res.status(400).json({ error: 'API URL and Key are required for embeddings' }); + } + + try { + // Read file content + let text = ''; + if (file.mimetype === 'application/pdf') { + text = `[PDF file: ${file.originalname}]`; // Simplified; in production use pdf-parse + } else { + text = fs.readFileSync(file.path, 'utf-8'); + } + + // Chunk the text + const chunks = chunkText(text, 500, 50); + if (chunks.length === 0) { + return res.status(400).json({ error: 'File is empty or could not be parsed' }); + } + + // Get embeddings + const embeddings = await getEmbeddings(chunks, apiUrl, apiKey, embeddingModel || 'text-embedding-3-small'); + + // Store in ChromaDB + const client = getChromaClient(); + const collection = await client.getCollection({ name: id }); + + const ids = chunks.map((_, i) => `${id}_chunk_${Date.now()}_${i}`); + const metadatas = chunks.map((chunk, i) => ({ + source: file.originalname, + chunkIndex: i, + fileId: file.filename, + })); + + await collection.add({ + ids, + embeddings, + documents: chunks, + metadatas, + }); + + // Update metadata + const fileInfo = { + id: file.filename, + originalName: file.originalname, + size: file.size, + chunkCount: chunks.length, + }; + + const files = knowledgeFiles.get(id) || []; + files.push(fileInfo); + knowledgeFiles.set(id, files); + + const kb = knowledgeBases.get(id); + if (kb) { + kb.documentCount = (kb.documentCount || 0) + chunks.length; + kb.files = files; + knowledgeBases.set(id, kb); + } + + res.json({ file: fileInfo, chunks: chunks.length }); + } catch (error) { + console.error('Upload to knowledge base error:', error); + res.status(500).json({ error: error.message }); + } +}); + +app.delete('/api/knowledge/:baseId/files/:fileId', async (req, res) => { + const { baseId, fileId } = req.params; + + try { + const client = getChromaClient(); + const collection = await client.getCollection({ name: baseId }); + + // Find all chunks with this fileId + const results = await collection.get({ + where: { fileId: { $eq: fileId } }, + }); + + if (results.ids.length > 0) { + await collection.delete({ ids: results.ids }); + } + + // Update metadata + const files = (knowledgeFiles.get(baseId) || []).filter((f) => f.id !== fileId); + knowledgeFiles.set(baseId, files); + + const kb = knowledgeBases.get(baseId); + if (kb) { + kb.files = files; + kb.documentCount = files.reduce((s, f) => s + (f.chunkCount || 0), 0); + knowledgeBases.set(baseId, kb); + } + + res.json({ success: true }); + } catch (error) { + console.error('Delete file from knowledge base error:', error); + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/knowledge/:id/query', async (req, res) => { + const { id } = req.params; + const { query, apiUrl, apiKey, embeddingModel, topK = 5 } = req.body; + + if (!query) return res.status(400).json({ error: 'Query is required' }); + if (!apiUrl || !apiKey) { + return res.status(400).json({ error: 'API URL and Key are required' }); + } + + try { + const [queryEmbedding] = await getEmbeddings([query], apiUrl, apiKey, embeddingModel || 'text-embedding-3-small'); + + const client = getChromaClient(); + const collection = await client.getCollection({ name: id }); + + const results = await collection.query({ + queryEmbeddings: [queryEmbedding], + nResults: Math.min(topK, 20), + include: ['documents', 'distances'], + }); + + const formatted = []; + if (results.documents[0]) { + for (let i = 0; i < results.documents[0].length; i++) { + formatted.push({ + text: results.documents[0][i], + score: 1 - (results.distances?.[0]?.[i] || 0), + }); + } + } + + res.json({ results: formatted }); + } catch (error) { + console.error('Query knowledge base error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Health check +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + app.listen(PORT, () => { - console.log(`AIUI Backend running on http://localhost:${PORT}`); + console.log(`AIUI Backend running on port ${PORT}`); + console.log(`Upload directory: ${uploadDir}`); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 83b3a63..c101443 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { useChatStore } from '@/stores/chatStore'; import Sidebar from '@/components/layout/Sidebar'; import ChatArea from '@/components/chat/ChatArea'; import SettingsModal from '@/components/chat/SettingsModal'; +import KnowledgePanel from '@/components/knowledge/KnowledgePanel'; import { Menu, Settings } from 'lucide-react'; function App() { @@ -51,7 +52,7 @@ function App() {
+
Press Enter to send, Shift+Enter for new line
{message.content}
+{message.content}
++ Model used for knowledge base embeddings +
++ Path to ChromaDB persistent storage on the server +
+
+ {content}
+
+ No knowledge bases yet
+Create one to start adding documents
+{base.description}
+ )} + + {/* Files */} + {base.files.length > 0 && ( ++ No knowledge bases yet +
+ ) : ( ++ Using: {knowledgeBases.find((b) => b.id === currentConversation.knowledgeBaseId)?.name} +
+ )} +