├── .commitlintrc.js ├── .editorconfig ├── .env.dist ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── commit-lint.yml │ ├── demo-deploy.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── .verdaccio └── config.yml ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── app.json ├── apps ├── api │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── app │ │ │ ├── app.module.ts │ │ │ ├── chat │ │ │ │ ├── agents │ │ │ │ │ ├── agents.module.ts │ │ │ │ │ ├── currency │ │ │ │ │ │ ├── currency.module.ts │ │ │ │ │ │ ├── currency.service.ts │ │ │ │ │ │ └── get-currency.agent.ts │ │ │ │ │ ├── pokemon │ │ │ │ │ │ ├── get-pokemon-list.agent.ts │ │ │ │ │ │ ├── get-pokemon-stats.agent.ts │ │ │ │ │ │ ├── pokemon.module.ts │ │ │ │ │ │ └── pokemon.service.ts │ │ │ │ │ └── weather │ │ │ │ │ │ ├── get-current-weather.agent.ts │ │ │ │ │ │ ├── weather.module.ts │ │ │ │ │ │ └── weather.service.ts │ │ │ │ ├── chat.config.ts │ │ │ │ ├── chat.module.ts │ │ │ │ └── chat.sockets.ts │ │ │ ├── cors.config.ts │ │ │ └── knowledge │ │ │ │ └── 33-things-to-ask-your-digital-product-development-partner.md │ │ ├── assets │ │ │ └── .gitkeep │ │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js └── spa │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── components │ │ │ ├── cards │ │ │ │ ├── card-footer │ │ │ │ │ ├── card-footer.component.html │ │ │ │ │ ├── card-footer.component.scss │ │ │ │ │ ├── card-footer.component.spec.ts │ │ │ │ │ └── card-footer.component.ts │ │ │ │ ├── card │ │ │ │ │ ├── card.component.html │ │ │ │ │ ├── card.component.scss │ │ │ │ │ ├── card.component.spec.ts │ │ │ │ │ └── card.component.ts │ │ │ │ └── index.ts │ │ │ ├── chat │ │ │ │ ├── chat-annotation │ │ │ │ │ ├── chat-annotation.component.html │ │ │ │ │ ├── chat-annotation.component.scss │ │ │ │ │ ├── chat-annotation.component.spec.ts │ │ │ │ │ └── chat-annotation.component.ts │ │ │ │ ├── chat-annotations │ │ │ │ │ ├── chat-annotations.component.html │ │ │ │ │ ├── chat-annotations.component.scss │ │ │ │ │ ├── chat-annotations.component.spec.ts │ │ │ │ │ └── chat-annotations.component.ts │ │ │ │ ├── chat-audio │ │ │ │ │ ├── chat-audio.component.html │ │ │ │ │ ├── chat-audio.component.scss │ │ │ │ │ ├── chat-audio.component.spec.ts │ │ │ │ │ └── chat-audio.component.ts │ │ │ │ ├── chat-avatar │ │ │ │ │ ├── chat-avatar.component.html │ │ │ │ │ ├── chat-avatar.component.scss │ │ │ │ │ ├── chat-avatar.component.spec.ts │ │ │ │ │ └── chat-avatar.component.ts │ │ │ │ ├── chat-content │ │ │ │ │ ├── chat-content.component.html │ │ │ │ │ ├── chat-content.component.scss │ │ │ │ │ ├── chat-content.component.spec.ts │ │ │ │ │ └── chat-content.component.ts │ │ │ │ ├── chat-footer │ │ │ │ │ ├── chat-footer.component.html │ │ │ │ │ ├── chat-footer.component.scss │ │ │ │ │ ├── chat-footer.component.spec.ts │ │ │ │ │ └── chat-footer.component.ts │ │ │ │ ├── chat-header │ │ │ │ │ ├── chat-header.component.html │ │ │ │ │ ├── chat-header.component.scss │ │ │ │ │ ├── chat-header.component.spec.ts │ │ │ │ │ └── chat-header.component.ts │ │ │ │ ├── chat-iframe-wrapper │ │ │ │ │ ├── chat-iframe-wrapper.component.html │ │ │ │ │ ├── chat-iframe-wrapper.component.scss │ │ │ │ │ ├── chat-iframe-wrapper.component.spec.ts │ │ │ │ │ └── chat-iframe-wrapper.component.ts │ │ │ │ ├── chat-message │ │ │ │ │ ├── chat-message.component.html │ │ │ │ │ ├── chat-message.component.scss │ │ │ │ │ ├── chat-message.component.spec.ts │ │ │ │ │ └── chat-message.component.ts │ │ │ │ ├── chat-messages │ │ │ │ │ ├── chat-messages.component.html │ │ │ │ │ ├── chat-messages.component.scss │ │ │ │ │ ├── chat-messages.component.spec.ts │ │ │ │ │ └── chat-messages.component.ts │ │ │ │ ├── chat-status │ │ │ │ │ ├── chat-status.component.html │ │ │ │ │ ├── chat-status.component.scss │ │ │ │ │ ├── chat-status.component.spec.ts │ │ │ │ │ └── chat-status.component.ts │ │ │ │ ├── chat-tip │ │ │ │ │ ├── chat-tip.component.html │ │ │ │ │ ├── chat-tip.component.scss │ │ │ │ │ ├── chat-tip.component.spec.ts │ │ │ │ │ └── chat-tip.component.ts │ │ │ │ ├── chat-tips │ │ │ │ │ ├── chat-tips.component.html │ │ │ │ │ ├── chat-tips.component.scss │ │ │ │ │ ├── chat-tips.component.spec.ts │ │ │ │ │ └── chat-tips.component.ts │ │ │ │ ├── chat-trigger │ │ │ │ │ ├── chat-trigger.component.html │ │ │ │ │ ├── chat-trigger.component.scss │ │ │ │ │ ├── chat-trigger.component.spec.ts │ │ │ │ │ └── chat-trigger.component.ts │ │ │ │ └── chat-typing │ │ │ │ │ ├── chat-typing.component.html │ │ │ │ │ ├── chat-typing.component.scss │ │ │ │ │ ├── chat-typing.component.spec.ts │ │ │ │ │ └── chat-typing.component.ts │ │ │ ├── controls │ │ │ │ ├── control-icon │ │ │ │ │ ├── control-icon.component.html │ │ │ │ │ ├── control-icon.component.scss │ │ │ │ │ ├── control-icon.component.spec.ts │ │ │ │ │ └── control-icon.component.ts │ │ │ │ ├── control-item │ │ │ │ │ ├── control-item.component.html │ │ │ │ │ ├── control-item.component.scss │ │ │ │ │ ├── control-item.component.spec.ts │ │ │ │ │ └── control-item.component.ts │ │ │ │ ├── controls │ │ │ │ │ ├── controls.component.html │ │ │ │ │ ├── controls.component.scss │ │ │ │ │ ├── controls.component.spec.ts │ │ │ │ │ └── controls.component.ts │ │ │ │ ├── files │ │ │ │ │ ├── files.component.html │ │ │ │ │ ├── files.component.scss │ │ │ │ │ ├── files.component.spec.ts │ │ │ │ │ ├── files.component.ts │ │ │ │ │ ├── files.directive.ts │ │ │ │ │ └── files.service.ts │ │ │ │ ├── index.ts │ │ │ │ ├── input │ │ │ │ │ ├── input.component.html │ │ │ │ │ ├── input.component.scss │ │ │ │ │ ├── input.component.spec.ts │ │ │ │ │ └── input.component.ts │ │ │ │ ├── message-content │ │ │ │ │ ├── message-content.component.html │ │ │ │ │ ├── message-content.component.scss │ │ │ │ │ ├── message-content.component.spec.ts │ │ │ │ │ ├── message-content.component.ts │ │ │ │ │ ├── message-content.helpers.ts │ │ │ │ │ └── message-content.service.ts │ │ │ │ └── recorder │ │ │ │ │ ├── recorder.component.html │ │ │ │ │ ├── recorder.component.scss │ │ │ │ │ ├── recorder.component.spec.ts │ │ │ │ │ ├── recorder.component.ts │ │ │ │ │ └── recordrtc.d.ts │ │ │ └── spinner │ │ │ │ ├── spinner.component.html │ │ │ │ ├── spinner.component.scss │ │ │ │ ├── spinner.component.spec.ts │ │ │ │ └── spinner.component.ts │ │ ├── modules │ │ │ ├── +chat │ │ │ │ ├── chat.routes.ts │ │ │ │ ├── containers │ │ │ │ │ ├── chat-cloud │ │ │ │ │ │ ├── chat-cloud.component.html │ │ │ │ │ │ ├── chat-cloud.component.scss │ │ │ │ │ │ ├── chat-cloud.component.spec.ts │ │ │ │ │ │ └── chat-cloud.component.ts │ │ │ │ │ ├── chat-home │ │ │ │ │ │ ├── chat-home.component.html │ │ │ │ │ │ ├── chat-home.component.scss │ │ │ │ │ │ ├── chat-home.component.spec.ts │ │ │ │ │ │ └── chat-home.component.ts │ │ │ │ │ ├── chat-iframe │ │ │ │ │ │ ├── chat-iframe.component.html │ │ │ │ │ │ ├── chat-iframe.component.scss │ │ │ │ │ │ ├── chat-iframe.component.spec.ts │ │ │ │ │ │ └── chat-iframe.component.ts │ │ │ │ │ ├── chat-integration │ │ │ │ │ │ ├── chat-integration.component.html │ │ │ │ │ │ ├── chat-integration.component.scss │ │ │ │ │ │ ├── chat-integration.component.spec.ts │ │ │ │ │ │ └── chat-integration.component.ts │ │ │ │ │ └── chat │ │ │ │ │ │ ├── chat.component.html │ │ │ │ │ │ ├── chat.component.scss │ │ │ │ │ │ ├── chat.component.spec.ts │ │ │ │ │ │ └── chat.component.ts │ │ │ │ └── shared │ │ │ │ │ ├── chat-client.service.ts │ │ │ │ │ ├── chat-files.service.ts │ │ │ │ │ ├── chat-gateway.service.ts │ │ │ │ │ ├── chat.helpers.ts │ │ │ │ │ ├── chat.model.ts │ │ │ │ │ ├── chat.service.ts │ │ │ │ │ ├── thread-client.service.ts │ │ │ │ │ └── thread.service.ts │ │ │ └── +configuration │ │ │ │ ├── components │ │ │ │ └── configuration-form │ │ │ │ │ ├── configuration-form.component.html │ │ │ │ │ ├── configuration-form.component.scss │ │ │ │ │ ├── configuration-form.component.spec.ts │ │ │ │ │ └── configuration-form.component.ts │ │ │ │ └── shared │ │ │ │ ├── configuration-form.service.ts │ │ │ │ ├── configuration.helpers.ts │ │ │ │ └── configuration.model.ts │ │ └── pipes │ │ │ ├── annotation.pipe.ts │ │ │ ├── message-file.pipe.ts │ │ │ └── message-text.pipe.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── ai-assistant-v2.svg │ │ ├── ai-assistant.jpg │ │ ├── ai-assistant.svg │ │ ├── ai.jpg │ │ ├── ai.svg │ │ ├── avatar.jpeg │ │ ├── boldare-circle.png │ │ ├── boldare.svg │ │ ├── js │ │ │ └── .gitkeep │ │ ├── trigger.jpeg │ │ └── uploads │ │ │ └── .gitkeep │ ├── environments │ │ ├── environment.development.ts │ │ └── environment.ts │ ├── index.html │ ├── main.ts │ ├── styles │ │ ├── _extends │ │ │ ├── _markdown.scss │ │ │ └── _material.scss │ │ ├── _settings │ │ │ ├── _animations.scss │ │ │ ├── _borders.scss │ │ │ ├── _breakpoints.scss │ │ │ ├── _colors.scss │ │ │ ├── _shadow.scss │ │ │ └── _sizes.scss │ │ ├── settings.scss │ │ └── styles.scss │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── jest.config.ts ├── jest.preset.js ├── libs ├── ai-embedded │ ├── .eslintrc.json │ ├── .swcrc │ ├── jest.config.ts │ ├── project.json │ ├── set-env.mjs │ ├── src │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── assistant-iframe.model.ts │ │ │ ├── assistant-iframe.styles.ts │ │ │ └── assistant-iframe.ts │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js └── openai-assistant │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── agent │ │ ├── agent.base.spec.ts │ │ ├── agent.base.ts │ │ ├── agent.mock.ts │ │ ├── agent.model.ts │ │ ├── agent.module.ts │ │ ├── agent.service.spec.ts │ │ ├── agent.service.ts │ │ └── index.ts │ │ ├── ai │ │ ├── ai.controller.spec.ts │ │ ├── ai.controller.ts │ │ ├── ai.mock.ts │ │ ├── ai.model.ts │ │ ├── ai.module.ts │ │ ├── ai.service.spec.ts │ │ ├── ai.service.ts │ │ └── index.ts │ │ ├── annotations │ │ ├── annotations.model.ts │ │ ├── annotations.utils.ts │ │ └── index.ts │ │ ├── assistant │ │ ├── assistant-files.service.spec.ts │ │ ├── assistant-files.service.ts │ │ ├── assistant-memory.service.spec.ts │ │ ├── assistant-memory.service.ts │ │ ├── assistant.controller.spec.ts │ │ ├── assistant.controller.ts │ │ ├── assistant.mock.ts │ │ ├── assistant.model.ts │ │ ├── assistant.module.ts │ │ ├── assistant.service.spec.ts │ │ ├── assistant.service.ts │ │ └── index.ts │ │ ├── chat │ │ ├── chat.controller.spec.ts │ │ ├── chat.controller.ts │ │ ├── chat.gateway.spec.ts │ │ ├── chat.gateway.ts │ │ ├── chat.helpers.spec.ts │ │ ├── chat.helpers.ts │ │ ├── chat.model.ts │ │ ├── chat.module.ts │ │ ├── chat.service.spec.ts │ │ ├── chat.service.ts │ │ └── index.ts │ │ ├── config │ │ ├── config.module.ts │ │ ├── config.service.spec.ts │ │ ├── config.service.ts │ │ └── index.ts │ │ ├── files │ │ ├── files.controller.spec.ts │ │ ├── files.controller.ts │ │ ├── files.model.ts │ │ ├── files.module.ts │ │ ├── files.service.spec.ts │ │ ├── files.service.ts │ │ └── index.ts │ │ ├── run │ │ ├── index.ts │ │ ├── run.module.ts │ │ ├── run.service.spec.ts │ │ └── run.service.ts │ │ ├── stream │ │ └── stream.utils.ts │ │ └── threads │ │ ├── index.ts │ │ ├── threads.controller.spec.ts │ │ ├── threads.controller.ts │ │ ├── threads.model.ts │ │ ├── threads.module.ts │ │ ├── threads.service.spec.ts │ │ └── threads.service.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── nx.json ├── package-lock.json ├── package.json ├── project.json ├── tools └── scripts │ └── publish.mjs └── tsconfig.base.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'header-max-length': [0, 'always', 120], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY= 3 | 4 | # Assistant ID - leave it empty if you don't have an assistant yet 5 | ASSISTANT_ID= 6 | ASSISTANT_IS_LOGGER_ENABLED= 7 | 8 | # For embedding the assistant in your website (build process) 9 | APP_URL= 10 | 11 | # Agents: 12 | # ------------------------------------------------------------------- 13 | # OpenWeather (Current Weather Data) 14 | OPENWEATHER_API_KEY= 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | apps/spa/src/assets 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": { 28 | "@typescript-eslint/no-extra-semi": "off", 29 | "no-extra-semi": "off" 30 | } 31 | }, 32 | { 33 | "files": ["*.js", "*.jsx"], 34 | "extends": ["plugin:@nx/javascript"], 35 | "rules": { 36 | "@typescript-eslint/no-extra-semi": "off", 37 | "no-extra-semi": "off" 38 | } 39 | }, 40 | { 41 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 42 | "env": { 43 | "jest": true 44 | }, 45 | "rules": {} 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Type 2 | 3 | What kind of change does this PR introduce? 4 | 5 | 6 | 7 | - [ ] Bugfix 8 | - [ ] Feature 9 | - [ ] Code style update (formatting, local variables) 10 | - [ ] Refactoring (no functional changes, no api changes) 11 | - [ ] Build related changes 12 | - [ ] CI related changes 13 | - [ ] Documentation content changes 14 | - [ ] Other... Please describe: 15 | 16 | ## What is the current behavior? 17 | 18 | 19 | 20 | Issue Number: N/A 21 | 22 | ## What is the new behavior? 23 | 24 | ## Does this PR introduce a breaking change? 25 | 26 | - [ ] Yes 27 | - [ ] No 28 | 29 | 30 | 31 | ## Other information 32 | -------------------------------------------------------------------------------- /.github/workflows/commit-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | 3 | on: 4 | pull_request: 5 | branches-ignore: 6 | - master 7 | - main 8 | - next 9 | 10 | permissions: 11 | contents: read 12 | pull-requests: read 13 | 14 | jobs: 15 | commitlint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: wagoid/commitlint-github-action@v5 20 | -------------------------------------------------------------------------------- /.github/workflows/demo-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Demo deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - preview 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: akhileshns/heroku-deploy@v3.13.15 16 | with: 17 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 18 | heroku_app_name: ${{secrets.HEROKU_APP_NAME}} 19 | heroku_email: ${{secrets.HEROKU_EMAIL}} 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: '20.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - run: npm ci 17 | - run: npm run publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test suite 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - main 8 | - next 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - name: Install dependencies 20 | run: npm install 21 | - name: Run unit tests 22 | run: npm run test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .nx/cache 42 | .nx/workspace-data 43 | .angular 44 | 45 | .env 46 | .env.* 47 | !.env.dist 48 | /apps/spa/src/assets/uploads 49 | /apps/spa/src/assets/js/*.js 50 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | .angular 6 | *.md 7 | 8 | /.nx/workspace-data -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "bracketSameLine": true, 10 | "printWidth": 80 11 | } 12 | -------------------------------------------------------------------------------- /.verdaccio/config.yml: -------------------------------------------------------------------------------- 1 | # path to a directory with all packages 2 | storage: ../tmp/local-registry/storage 3 | 4 | # a list of other known repositories we can talk to 5 | uplinks: 6 | npmjs: 7 | url: https://registry.npmjs.org/ 8 | maxage: 60m 9 | 10 | packages: 11 | '**': 12 | # give all users (including non-authenticated users) full access 13 | # because it is a local registry 14 | access: $all 15 | publish: $all 16 | unpublish: $all 17 | 18 | # if package is not available locally, proxy requests to npm registry 19 | proxy: npmjs 20 | 21 | # log settings 22 | logs: 23 | type: stdout 24 | format: pretty 25 | level: warn 26 | 27 | publish: 28 | allow_offline: true # set offline to true to allow publish offline 29 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Boldare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Openai Assistant by Boldare", 3 | "description": "A NestJS project for OpenAI Assistant powered by Boldare", 4 | "repository": "https://github.com/boldare/openai-assistant", 5 | "logo": "https://assistant.ai.boldare.dev/assets/ai-assistant.jpg", 6 | "keywords": ["nestjs", "openai", "assistant", "boldare", "ai", "chatbot", "assistant-ai"], 7 | "env": { 8 | "OPENAI_API_KEY": { 9 | "description": "API key for OpenAI. You can generate and find it in the OpenAI dashboard.", 10 | "required": true 11 | }, 12 | "ASSISTANT_ID": { 13 | "description": "Assistant ID has to be defined for Heroku deployment. You can create and find it in the OpenAI dashboard.", 14 | "required": true 15 | }, 16 | "APP_URL": { 17 | "description": "URL of your application - required only if you want to embed the chatbot to the different domains", 18 | "required": false 19 | }, 20 | "OPENWEATHER_API_KEY": { 21 | "description": "API key for OpenWeather - required only if you want to use weather tool", 22 | "required": false 23 | } 24 | }, 25 | "scripts": {}, 26 | "formation": { 27 | "web": { 28 | "quantity": 1 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/api/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'api', 4 | preset: '../../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../../coverage/apps/api', 11 | }; 12 | -------------------------------------------------------------------------------- /apps/api/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/api/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/webpack:webpack", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "target": "node", 13 | "compiler": "tsc", 14 | "outputPath": "dist/apps/api", 15 | "main": "apps/api/src/main.ts", 16 | "tsConfig": "apps/api/tsconfig.app.json", 17 | "assets": ["apps/api/src/assets"], 18 | "webpackConfig": "apps/api/webpack.config.js" 19 | }, 20 | "configurations": { 21 | "development": {}, 22 | "production": {} 23 | } 24 | }, 25 | "serve": { 26 | "executor": "@nx/js:node", 27 | "defaultConfiguration": "development", 28 | "options": { 29 | "buildTarget": "api:build" 30 | }, 31 | "configurations": { 32 | "development": { 33 | "buildTarget": "api:build:development" 34 | }, 35 | "production": { 36 | "buildTarget": "api:build:production" 37 | } 38 | } 39 | }, 40 | "lint": { 41 | "executor": "@nx/eslint:lint", 42 | "outputs": ["{options.outputFile}"] 43 | }, 44 | "test": { 45 | "executor": "@nx/jest:jest", 46 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 47 | "options": { 48 | "jestConfig": "apps/api/jest.config.ts" 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /apps/api/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ChatModule } from './chat/chat.module'; 3 | import { ServeStaticModule } from '@nestjs/serve-static'; 4 | import { join } from 'path'; 5 | 6 | @Module({ 7 | imports: [ 8 | ChatModule, 9 | ServeStaticModule.forRoot({ 10 | rootPath: join(__dirname, '..', 'spa'), 11 | exclude: ['/api*'], 12 | }), 13 | ], 14 | }) 15 | export class AppModule {} 16 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/agents.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WeatherModule } from './weather/weather.module'; 3 | import { PokemonModule } from './pokemon/pokemon.module'; 4 | import { CurrencyModule } from './currency/currency.module'; 5 | 6 | @Module({ 7 | imports: [WeatherModule, PokemonModule, CurrencyModule], 8 | }) 9 | export class AgentsModule {} 10 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/currency/currency.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GetCurrencyAgent } from './get-currency.agent'; 3 | import { HttpModule } from '@nestjs/axios'; 4 | import { CurrencyService } from './currency.service'; 5 | import { AgentModule } from '@boldare/openai-assistant'; 6 | 7 | @Module({ 8 | imports: [AgentModule, HttpModule], 9 | providers: [CurrencyService, GetCurrencyAgent], 10 | }) 11 | export class CurrencyModule {} 12 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/currency/currency.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable, Logger } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { catchError, firstValueFrom } from 'rxjs'; 4 | import { AxiosError } from 'axios'; 5 | 6 | @Injectable() 7 | export class CurrencyService { 8 | private readonly logger = new Logger(CurrencyService.name); 9 | 10 | constructor(private httpService: HttpService) {} 11 | 12 | async getExchangeRate(currency: string) { 13 | const params = { from: currency }; 14 | const { data } = await firstValueFrom( 15 | this.httpService 16 | .get('https://api.frankfurter.app/latest', { params }) 17 | .pipe( 18 | catchError((error: AxiosError) => { 19 | const message = error?.response?.data || { 20 | message: 'Unknown error', 21 | }; 22 | this.logger.error(message); 23 | throw new HttpException(message, error?.response?.status || 500); 24 | }), 25 | ), 26 | ); 27 | 28 | return data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/currency/get-currency.agent.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FunctionTool } from 'openai/resources/beta'; 3 | import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; 4 | import { CurrencyService } from './currency.service'; 5 | 6 | @Injectable() 7 | export class GetCurrencyAgent extends AgentBase { 8 | override definition: FunctionTool = { 9 | type: 'function', 10 | function: { 11 | name: this.constructor.name, 12 | description: 'Get the current currency exchange rate.', 13 | parameters: { 14 | type: 'object', 15 | properties: { 16 | currency: { 17 | type: 'string', 18 | description: 'Currency code e.g. USD, EUR, GBP, etc.', 19 | }, 20 | }, 21 | required: ['currency'], 22 | }, 23 | }, 24 | }; 25 | 26 | constructor( 27 | override readonly agentService: AgentService, 28 | private readonly currencyService: CurrencyService, 29 | ) { 30 | super(agentService); 31 | } 32 | 33 | override async output(data: AgentData): Promise { 34 | try { 35 | // Parse the parameters from the input data 36 | const params = JSON.parse(data.params); 37 | const currency = params?.currency; 38 | 39 | // Check if the currency is provided 40 | if (!currency) { 41 | return 'No currency provided'; 42 | } 43 | 44 | // Get the current currency exchange rate 45 | const response = await this.currencyService.getExchangeRate(currency); 46 | 47 | // Return the result 48 | return `The current exchange rate for ${currency} is: ${JSON.stringify( 49 | response, 50 | )}`; 51 | } catch (errors) { 52 | // Handle the errors 53 | return `Invalid data: ${JSON.stringify(errors)}`; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/pokemon/get-pokemon-list.agent.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AgentBase, AgentService } from '@boldare/openai-assistant'; 3 | import { PokemonService } from './pokemon.service'; 4 | import { FunctionTool } from 'openai/resources/beta'; 5 | 6 | @Injectable() 7 | export class GetPokemonListAgent extends AgentBase { 8 | override definition: FunctionTool = { 9 | type: 'function', 10 | function: { 11 | name: this.constructor.name, 12 | description: 'Get list of Pokemon.', 13 | parameters: { 14 | type: 'object', 15 | properties: {}, 16 | required: [], 17 | }, 18 | }, 19 | }; 20 | 21 | constructor( 22 | override readonly agentService: AgentService, 23 | private readonly pokemonService: PokemonService, 24 | ) { 25 | super(agentService); 26 | } 27 | 28 | override async output(): Promise { 29 | try { 30 | // Get the list of Pokemon 31 | const pokemon = await this.pokemonService.getPokemonList(); 32 | 33 | // Return the result 34 | return `The list of Pokemon: ${JSON.stringify(pokemon)}`; 35 | } catch (errors) { 36 | // Handle the errors 37 | return `Invalid data: ${JSON.stringify(errors)}`; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/pokemon/get-pokemon-stats.agent.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FunctionTool } from 'openai/resources/beta'; 3 | import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; 4 | import { PokemonService } from './pokemon.service'; 5 | 6 | @Injectable() 7 | export class GetPokemonStatsAgent extends AgentBase { 8 | override definition: FunctionTool = { 9 | type: 'function', 10 | function: { 11 | name: this.constructor.name, 12 | description: 'Get the stats of a Pokemon', 13 | parameters: { 14 | type: 'object', 15 | properties: { 16 | name: { 17 | type: 'string', 18 | description: 19 | 'Name of the Pokemon e.g. Pikachu, Bulbasaur, Charmander, etc.', 20 | }, 21 | }, 22 | required: ['name'], 23 | }, 24 | }, 25 | }; 26 | 27 | constructor( 28 | override readonly agentService: AgentService, 29 | private readonly pokemonService: PokemonService, 30 | ) { 31 | super(agentService); 32 | } 33 | 34 | override async output(data: AgentData): Promise { 35 | try { 36 | // Parse the parameters from the input data 37 | const params = JSON.parse(data.params); 38 | const name = params?.name; 39 | 40 | // Check if the name is provided 41 | if (!name) { 42 | return 'No name provided'; 43 | } 44 | 45 | // Get the stats for the Pokemon 46 | const pokemon = await this.pokemonService.getPokemon(name); 47 | 48 | // Return the result 49 | return `The stats of ${name} are: ${JSON.stringify(pokemon)}`; 50 | } catch (errors) { 51 | // Handle the errors 52 | return `Invalid data: ${JSON.stringify(errors)}`; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/pokemon/pokemon.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { AgentModule } from '@boldare/openai-assistant'; 4 | import { GetPokemonStatsAgent } from './get-pokemon-stats.agent'; 5 | import { PokemonService } from './pokemon.service'; 6 | import { GetPokemonListAgent } from './get-pokemon-list.agent'; 7 | 8 | @Module({ 9 | imports: [AgentModule, HttpModule], 10 | providers: [PokemonService, GetPokemonStatsAgent, GetPokemonListAgent], 11 | }) 12 | export class PokemonModule {} 13 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/pokemon/pokemon.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable, Logger } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { catchError, firstValueFrom } from 'rxjs'; 4 | import { AxiosError } from 'axios'; 5 | 6 | @Injectable() 7 | export class PokemonService { 8 | private readonly logger = new Logger(PokemonService.name); 9 | private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon'; 10 | 11 | constructor(private httpService: HttpService) {} 12 | 13 | async getPokemonList() { 14 | const { data } = await firstValueFrom( 15 | this.httpService.get(`${this.apiUrl}/?limit=30`).pipe( 16 | catchError((error: AxiosError) => { 17 | const message = error?.response?.data || { message: 'Unknown error' }; 18 | this.logger.error(message); 19 | throw new HttpException(message, error?.response?.status || 500); 20 | }), 21 | ), 22 | ); 23 | 24 | return data; 25 | } 26 | 27 | async getPokemon(name: string) { 28 | const { data } = await firstValueFrom( 29 | this.httpService.get(`${this.apiUrl}/${name?.toLowerCase()}`).pipe( 30 | catchError((error: AxiosError) => { 31 | const message = error?.response?.data || { message: 'Unknown error' }; 32 | this.logger.error(message); 33 | throw new HttpException(message, error?.response?.status || 500); 34 | }), 35 | ), 36 | ); 37 | 38 | return data; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/weather/get-current-weather.agent.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FunctionTool } from 'openai/resources/beta'; 3 | import { AgentBase, AgentData, AgentService } from '@boldare/openai-assistant'; 4 | import { WeatherService } from './weather.service'; 5 | 6 | @Injectable() 7 | export class GetCurrentWeatherAgent extends AgentBase { 8 | override definition: FunctionTool = { 9 | type: 'function', 10 | function: { 11 | name: this.constructor.name, 12 | description: 'Get the current weather in location', 13 | parameters: { 14 | type: 'object', 15 | properties: { 16 | city: { 17 | type: 'string', 18 | description: 19 | 'Name of the city e.g. Warsaw, San Francisco, Paris, etc.', 20 | }, 21 | }, 22 | required: ['city'], 23 | }, 24 | }, 25 | }; 26 | 27 | constructor( 28 | override readonly agentService: AgentService, 29 | private readonly weatherService: WeatherService, 30 | ) { 31 | super(agentService); 32 | } 33 | 34 | override async output(data: AgentData): Promise { 35 | try { 36 | // Parse the parameters from the input data 37 | const params = JSON.parse(data.params); 38 | const city = params?.city; 39 | 40 | // Check if the city is provided 41 | if (!city) { 42 | return 'No city provided'; 43 | } 44 | 45 | // Get the current weather for the city 46 | const weather = await this.weatherService.getCurrentWeather(city); 47 | 48 | // Return the result 49 | return `The current weather in ${city} is: ${JSON.stringify(weather)}`; 50 | } catch (errors) { 51 | // Handle the errors 52 | return `Invalid data: ${JSON.stringify(errors)}`; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/weather/weather.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GetCurrentWeatherAgent } from './get-current-weather.agent'; 3 | import { HttpModule } from '@nestjs/axios'; 4 | import { WeatherService } from './weather.service'; 5 | import { AgentModule } from '@boldare/openai-assistant'; 6 | 7 | @Module({ 8 | imports: [AgentModule, HttpModule], 9 | providers: [WeatherService, GetCurrentWeatherAgent], 10 | }) 11 | export class WeatherModule {} 12 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/agents/weather/weather.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable, Logger } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { catchError, firstValueFrom } from 'rxjs'; 4 | import { AxiosError } from 'axios'; 5 | 6 | @Injectable() 7 | export class WeatherService { 8 | private readonly logger = new Logger(WeatherService.name); 9 | 10 | constructor(private httpService: HttpService) {} 11 | 12 | async getCurrentWeather(city: string) { 13 | const params = { 14 | q: city, 15 | appid: process.env['OPENWEATHER_API_KEY'] || '', 16 | }; 17 | 18 | const { data } = await firstValueFrom( 19 | this.httpService 20 | .get('https://api.openweathermap.org/data/2.5/weather', { params }) 21 | .pipe( 22 | catchError((error: AxiosError) => { 23 | const message = error?.response?.data || { 24 | message: 'Unknown error', 25 | }; 26 | this.logger.error(message); 27 | throw new HttpException(message, error?.response?.status || 500); 28 | }), 29 | ), 30 | ); 31 | 32 | return data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/chat.config.ts: -------------------------------------------------------------------------------- 1 | import { AssistantCreateParams } from 'openai/resources/beta'; 2 | import { AssistantConfigParams } from '@boldare/openai-assistant'; 3 | import 'dotenv/config'; 4 | 5 | export const assistantParams: AssistantCreateParams = { 6 | name: '@boldare/openai-assistant', 7 | instructions: `You are a chatbot assistant. Use the general knowledge to answer questions. Speak briefly and clearly.`, 8 | tools: [{ type: 'code_interpreter' }, { type: 'file_search' }], 9 | model: 'gpt-4o-mini', 10 | temperature: 0.05, 11 | }; 12 | 13 | export const assistantConfig: AssistantConfigParams = { 14 | id: process.env['ASSISTANT_ID'] || '', 15 | params: assistantParams, 16 | filesDir: './apps/api/src/app/knowledge', 17 | toolResources: { 18 | fileSearch: { 19 | fileNames: ['33-things-to-ask-your-digital-product-development-partner.md'], 20 | }, 21 | codeInterpreter: { 22 | fileNames: [], 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AssistantModule } from '@boldare/openai-assistant'; 3 | import { assistantConfig } from './chat.config'; 4 | import { AgentsModule } from './agents/agents.module'; 5 | import { ChatSockets } from './chat.sockets'; 6 | 7 | @Module({ 8 | imports: [AgentsModule, AssistantModule.forRoot(assistantConfig)], 9 | providers: [ChatSockets], 10 | }) 11 | export class ChatModule {} 12 | -------------------------------------------------------------------------------- /apps/api/src/app/chat/chat.sockets.ts: -------------------------------------------------------------------------------- 1 | import { ChatGateway, ChatService } from '@boldare/openai-assistant'; 2 | import { WebSocketGateway } from '@nestjs/websockets'; 3 | import { cors } from '../cors.config'; 4 | 5 | @WebSocketGateway({ cors }) 6 | export class ChatSockets extends ChatGateway { 7 | constructor(override readonly chatsService: ChatService) { 8 | super(chatsService); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/app/cors.config.ts: -------------------------------------------------------------------------------- 1 | export const isDevelopment = process.env['NODE_ENV'] === 'development'; 2 | export const cors = { 3 | origin: isDevelopment ? '*' : true, 4 | methods: ['GET', 'POST'], 5 | credentials: true, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/api/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/api/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import { AppModule } from './app/app.module'; 5 | import { cors } from './app/cors.config'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | const globalPrefix = 'api'; 10 | const config = new DocumentBuilder() 11 | .setTitle('@boldare/openai-assistant') 12 | .setVersion('1.2.1') 13 | .build(); 14 | 15 | app.setGlobalPrefix(globalPrefix); 16 | 17 | app.enableCors(cors); 18 | 19 | const document = SwaggerModule.createDocument(app, config); 20 | 21 | SwaggerModule.setup('api/docs', app, document); 22 | 23 | const port = process.env['PORT'] || 3000; 24 | await app.listen(port); 25 | 26 | Logger.log( 27 | `🚀 Application is running on: http://localhost:${port}/${globalPrefix}`, 28 | ); 29 | } 30 | 31 | bootstrap(); 32 | -------------------------------------------------------------------------------- /apps/api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"], 7 | "emitDecoratorMetadata": true, 8 | "target": "es2021" 9 | }, 10 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 11 | "include": ["src/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "esModuleInterop": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/api/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { composePlugins, withNx } = require('@nx/webpack'); 2 | 3 | // Nx plugins for webpack. 4 | module.exports = composePlugins( 5 | withNx({ 6 | target: 'node', 7 | }), 8 | (config) => { 9 | // Update the webpack config as needed here. 10 | // e.g. `config.plugins.push(new MyPlugin())` 11 | return config; 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /apps/spa/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ai", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ai", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/spa/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'spa', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: '../../coverage/apps/spa', 7 | transform: { 8 | '^.+\\.(ts|mjs|js|html)$': [ 9 | 'jest-preset-angular', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$', 13 | }, 14 | ], 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /apps/spa/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterOutlet } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'ai-root', 7 | standalone: true, 8 | imports: [CommonModule, RouterOutlet], 9 | templateUrl: './app.component.html', 10 | }) 11 | export class AppComponent {} 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideAnimations } from '@angular/platform-browser/animations'; 6 | import { provideHttpClient } from '@angular/common/http'; 7 | import { provideMarkdown } from 'ngx-markdown'; 8 | 9 | export const appConfig: ApplicationConfig = { 10 | providers: [ 11 | provideRouter(routes), 12 | provideAnimations(), 13 | provideHttpClient(), 14 | provideMarkdown(), 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /apps/spa/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | children: [ 7 | { 8 | path: '', 9 | loadChildren: () => 10 | import('./modules/+chat/chat.routes').then(m => m.routes), 11 | }, 12 | ], 13 | }, 14 | { 15 | path: '**', 16 | redirectTo: '/', 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card-footer/card-footer.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card-footer/card-footer.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | gap: $size-2; 8 | min-height: 72px; 9 | padding: $size-3; 10 | box-sizing: border-box; 11 | } 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card-footer/card-footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CardFooterComponent } from './card-footer.component'; 4 | 5 | describe('CardFooterComponent', () => { 6 | let component: CardFooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CardFooterComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(CardFooterComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card-footer/card-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-card-footer', 5 | standalone: true, 6 | templateUrl: './card-footer.component.html', 7 | styleUrl: './card-footer.component.scss', 8 | }) 9 | export class CardFooterComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card/card.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card/card.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | width: 100%; 5 | background-color: var(--color-white); 6 | box-shadow: var(--shadow-default); 7 | border-radius: var(--border-radius-medium); 8 | } 9 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card/card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CardComponent } from './card.component'; 4 | 5 | describe('CardComponent', () => { 6 | let component: CardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CardComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(CardComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-card', 5 | standalone: true, 6 | templateUrl: './card.component.html', 7 | styleUrl: './card.component.scss', 8 | }) 9 | export class CardComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/cards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card/card.component'; 2 | export * from '../chat/chat-content/chat-content.component'; 3 | export * from './card-footer/card-footer.component'; 4 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotation/chat-annotation.component.html: -------------------------------------------------------------------------------- 1 |
6 | [{{ index }}] 7 |
8 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotation/chat-annotation.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | .annotation { 4 | cursor: pointer; 5 | 6 | &:hover { 7 | background-color: rgba(0, 0, 0, 0.15); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotation/chat-annotation.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { ChatAnnotationComponent } from './chat-annotation.component'; 3 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 4 | 5 | describe('ChatAnnotationComponent', () => { 6 | let component: ChatAnnotationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatAnnotationComponent, HttpClientTestingModule], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatAnnotationComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotation/chat-annotation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { MatTooltip } from '@angular/material/tooltip'; 3 | import { Annotation } from 'openai/resources/beta/threads'; 4 | import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service'; 5 | import { FileObject } from 'openai/resources'; 6 | import { isFileCitation } from '../../../pipes/annotation.pipe'; 7 | import { take } from 'rxjs'; 8 | 9 | @Component({ 10 | selector: 'ai-chat-annotation', 11 | standalone: true, 12 | templateUrl: './chat-annotation.component.html', 13 | styleUrl: './chat-annotation.component.scss', 14 | imports: [MatTooltip], 15 | }) 16 | export class ChatAnnotationComponent { 17 | @Input() annotation!: Annotation; 18 | @Input() index = 1; 19 | fileDetails!: FileObject; 20 | 21 | get fileId(): string { 22 | return isFileCitation(this.annotation) 23 | ? this.annotation.file_citation.file_id 24 | : this.annotation.file_path.file_id; 25 | } 26 | 27 | constructor(private chatClientService: ChatClientService) {} 28 | 29 | showDetails() { 30 | if (!this.fileId || !!this.fileDetails) { 31 | return; 32 | } 33 | 34 | this.chatClientService 35 | .retriveFile(this.fileId) 36 | .pipe(take(1)) 37 | .subscribe(details => (this.fileDetails = details)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotations/chat-annotations.component.html: -------------------------------------------------------------------------------- 1 | @if (message && message.type === 'text' && message.text.annotations.length) { 2 |
Annotations:
3 |
4 | @for ( 5 | annotation of message.text.annotations; 6 | track annotation.text + $index 7 | ) { 8 | 9 | [{{ $index + 1 }}] 10 | 11 | } 12 |
13 | } 14 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotations/chat-annotations.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | align-items: baseline; 6 | gap: $size-2; 7 | border-top: 1px dashed var(--color-grey-500); 8 | margin-top: $size-3; 9 | padding-top: $size-3; 10 | font-size: 12px; 11 | 12 | &:empty { 13 | display: none; 14 | } 15 | } 16 | 17 | .title { 18 | font-weight: 500; 19 | } 20 | 21 | .content { 22 | display: flex; 23 | gap: $size-1; 24 | margin-top: $size-2; 25 | } 26 | 27 | .annotation { 28 | cursor: pointer; 29 | 30 | &:hover { 31 | background-color: rgba(0, 0, 0, 0.15); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotations/chat-annotations.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MarkdownModule } from 'ngx-markdown'; 3 | import { ChatAnnotationsComponent } from './chat-annotations.component'; 4 | 5 | describe('ChatAnnotationsComponent', () => { 6 | let component: ChatAnnotationsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatAnnotationsComponent, MarkdownModule.forRoot()], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatAnnotationsComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-annotations/chat-annotations.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { MatTooltip } from '@angular/material/tooltip'; 3 | import { MessageContent } from 'openai/resources/beta/threads'; 4 | import { ChatAnnotationComponent } from '../chat-annotation/chat-annotation.component'; 5 | 6 | @Component({ 7 | selector: 'ai-chat-annotations', 8 | standalone: true, 9 | templateUrl: './chat-annotations.component.html', 10 | styleUrl: './chat-annotations.component.scss', 11 | imports: [MatTooltip, ChatAnnotationComponent], 12 | }) 13 | export class ChatAnnotationsComponent { 14 | @Input() message!: MessageContent; 15 | } 16 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-audio/chat-audio.component.html: -------------------------------------------------------------------------------- 1 | @if (isAudioEnabled && message && (message | messageText)) { 2 | 3 | @if (!isStarted) { 4 | play_circle 5 | } @else { 6 | pause_circle 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-audio/chat-audio.component.scss: -------------------------------------------------------------------------------- 1 | .chat-audio { 2 | position: absolute; 3 | top: 100%; 4 | right: -8px; 5 | margin-top: -16px; 6 | 7 | mat-icon { 8 | color: var(--color-grey-500); 9 | transition: var(--animation-fast); 10 | cursor: pointer; 11 | 12 | &:hover { 13 | color: var(--color-grey-900); 14 | } 15 | } 16 | } 17 | 18 | .chat-audio--user { 19 | margin-top: -16px; 20 | left: -8px; 21 | right: auto; 22 | } 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-audio/chat-audio.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatAudioComponent } from './chat-audio.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | 6 | describe('ChatAudioComponent', () => { 7 | let component: ChatAudioComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [HttpClientTestingModule, ChatAudioComponent], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ChatAudioComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-avatar/chat-avatar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-avatar/chat-avatar.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | .chat-avatar { 4 | position: relative; 5 | box-shadow: var(--shadow-default); 6 | border-radius: 50%; 7 | width: 24px; 8 | height: 24px; 9 | background-image: url('/assets/avatar.jpeg'); 10 | background-size: cover; 11 | background-position: center; 12 | } 13 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-avatar/chat-avatar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatAvatarComponent } from './chat-avatar.component'; 4 | 5 | describe('ChatAvatarComponent', () => { 6 | let component: ChatAvatarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatAvatarComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatAvatarComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-avatar/chat-avatar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ChatStatusComponent } from '../chat-status/chat-status.component'; 3 | 4 | @Component({ 5 | selector: 'ai-chat-avatar', 6 | standalone: true, 7 | templateUrl: './chat-avatar.component.html', 8 | styleUrl: './chat-avatar.component.scss', 9 | imports: [ChatStatusComponent], 10 | }) 11 | export class ChatAvatarComponent {} 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-content/chat-content.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-content/chat-content.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | justify-content: center; 6 | flex-direction: column; 7 | padding: $size-6 $size-3; 8 | box-sizing: border-box; 9 | } 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-content/chat-content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatContentComponent } from './chat-content.component'; 4 | 5 | describe('CardContentComponent', () => { 6 | let component: ChatContentComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatContentComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatContentComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-content/chat-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-chat-content', 5 | standalone: true, 6 | templateUrl: './chat-content.component.html', 7 | styleUrl: './chat-content.component.scss', 8 | }) 9 | export class ChatContentComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-footer/chat-footer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | @if (isTranscriptionEnabled) { 4 | 8 | } 9 | @if (isAttachmentEnabled) { 10 | 13 | } 14 | @if (isImageContentEnabled) { 15 | 18 | } 19 | 20 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-footer/chat-footer.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: block; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-footer/chat-footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatFooterComponent } from './chat-footer.component'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | describe('ChatFooterComponent', () => { 7 | let component: ChatFooterComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [ChatFooterComponent, BrowserAnimationsModule], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ChatFooterComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-footer/chat-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { CardFooterComponent } from '../../cards'; 3 | import { 4 | ControlsComponent, 5 | FilesComponent, 6 | InputComponent, 7 | RecorderComponent, 8 | } from '../../controls'; 9 | import { MatTooltip } from '@angular/material/tooltip'; 10 | import { MessageContentComponent } from '../../controls/message-content/message-content.component'; 11 | 12 | @Component({ 13 | selector: 'ai-chat-footer', 14 | standalone: true, 15 | templateUrl: './chat-footer.component.html', 16 | styleUrl: './chat-footer.component.scss', 17 | imports: [ 18 | ControlsComponent, 19 | CardFooterComponent, 20 | InputComponent, 21 | RecorderComponent, 22 | FilesComponent, 23 | MatTooltip, 24 | MessageContentComponent, 25 | ], 26 | }) 27 | export class ChatFooterComponent { 28 | @Output() sendAudio$ = new EventEmitter(); 29 | @Output() sendMessage$ = new EventEmitter(); 30 | @Input() isDisabled = false; 31 | @Input() isTranscriptionEnabled = false; 32 | @Input() isAttachmentEnabled = false; 33 | @Input() isImageContentEnabled = false; 34 | } 35 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-header/chat-header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | 15 | @if (isConfigEnabled) { 16 | 19 | } 20 | @if (isRefreshEnabled) { 21 | 28 | } 29 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-header/chat-header.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | position: relative; 5 | display: block; 6 | width: 100%; 7 | z-index: 5; 8 | } 9 | 10 | .chat-header { 11 | display: flex; 12 | align-items: center; 13 | flex-direction: row; 14 | justify-content: space-between; 15 | gap: 10px; 16 | width: 100%; 17 | box-shadow: var(--shadow-default); 18 | padding: $size-4 $size-2 $size-4 $size-6; 19 | border-radius: var(--border-radius-medium) var(--border-radius-medium) 0 0; 20 | box-sizing: border-box; 21 | } 22 | 23 | .chat-header__img { 24 | display: block; 25 | max-height: 100px; 26 | height: 16px; 27 | } 28 | 29 | .char-header__icon { 30 | height: 18px; 31 | width: 18px; 32 | font-size: 18px; 33 | color: var(--color-grey-500); 34 | } 35 | 36 | .chat-header__controls { 37 | display: flex; 38 | } 39 | 40 | .chat-header__logo { 41 | position: relative; 42 | } 43 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-header/chat-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatHeaderComponent } from './chat-header.component'; 4 | 5 | describe('ChatHeaderComponent', () => { 6 | let component: ChatHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatHeaderComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatHeaderComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-header/chat-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { MatIcon } from '@angular/material/icon'; 3 | import { MatIconButton } from '@angular/material/button'; 4 | import { MatTooltip } from '@angular/material/tooltip'; 5 | import { RouterLink } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'ai-chat-header', 9 | standalone: true, 10 | imports: [MatIcon, MatIconButton, MatTooltip, RouterLink], 11 | templateUrl: './chat-header.component.html', 12 | styleUrl: './chat-header.component.scss', 13 | }) 14 | export class ChatHeaderComponent { 15 | @Output() close$ = new EventEmitter(); 16 | @Output() refresh$ = new EventEmitter(); 17 | @Output() config$ = new EventEmitter(); 18 | @Output() changeView$ = new EventEmitter(); 19 | @Input() isResponding = false; 20 | @Input() isRefreshEnabled = true; 21 | @Input() isConfigEnabled = true; 22 | 23 | close(): void { 24 | this.close$.emit(); 25 | parent.postMessage({ type: 'chatbot.close' }, '*'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-iframe-wrapper/chat-iframe-wrapper.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-iframe-wrapper/chat-iframe-wrapper.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: block; 5 | position: fixed; 6 | bottom: 20px; 7 | right: 20px; 8 | max-width: 440px; 9 | width: 100%; 10 | height: 85vh; 11 | min-height: 300px; 12 | max-height: 600px; 13 | margin: auto; 14 | box-sizing: border-box; 15 | border-radius: var(--border-radius-medium); 16 | box-shadow: var(--shadow-default); 17 | } 18 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-iframe-wrapper/chat-iframe-wrapper.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatIframeWrapperComponent } from './chat-iframe-wrapper.component'; 4 | 5 | describe('ChatIframeWrapperComponent', () => { 6 | let component: ChatIframeWrapperComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatIframeWrapperComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatIframeWrapperComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-iframe-wrapper/chat-iframe-wrapper.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-chat-iframe-wrapper', 5 | standalone: true, 6 | templateUrl: './chat-iframe-wrapper.component.html', 7 | styleUrl: './chat-iframe-wrapper.component.scss', 8 | }) 9 | export class ChatIframeWrapperComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-message/chat-message.component.html: -------------------------------------------------------------------------------- 1 | @if (message) { 2 | @if (message.role === 'assistant') { 3 | 4 | } 5 | 6 |
7 | @for (msg of message.content; track $index) { 8 | @if (msg.type === 'text') { 9 | 10 | } 11 | 12 | } 13 | 14 | @if ((message | messageImageFile).length) { 15 |
16 | @for ( 17 | image of message | messageImageFile; 18 | track image.image_file.file_id 19 | ) { 20 |
File ID: {{ image.image_file.file_id }}
21 | } 22 |
23 | } 24 |
25 | } 26 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-message/chat-message.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | flex-direction: row; 6 | gap: $size-2; 7 | align-items: flex-end; 8 | width: 100%; 9 | 10 | &.is-user { 11 | justify-content: flex-end; 12 | 13 | .chat-message { 14 | background: var(--color-primary-200); 15 | border-bottom-right-radius: 0; 16 | align-self: flex-end; 17 | } 18 | } 19 | 20 | &.is-system { 21 | justify-content: center; 22 | font-size: 12px; 23 | 24 | .chat-message { 25 | background-color: var(--color-transparent); 26 | color: var(--color-grey-400); 27 | align-self: center; 28 | text-align: center; 29 | } 30 | } 31 | 32 | &.is-assistant .chat-message { 33 | border-bottom-left-radius: 0; 34 | } 35 | } 36 | 37 | .chat-message { 38 | position: relative; 39 | background: var(--color-grey-200); 40 | border-radius: var(--border-radius-medium); 41 | display: inline-flex; 42 | flex-direction: column; 43 | padding: $size-3 $size-4; 44 | width: fit-content; 45 | max-width: 80%; 46 | z-index: 1; 47 | } 48 | 49 | .chat-message__file { 50 | border-top: 1px dashed rgba(0, 0, 0, 0.4); 51 | margin-top: $size-2; 52 | padding-top: $size-2; 53 | font-size: 11px; 54 | } 55 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-message/chat-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MarkdownModule } from 'ngx-markdown'; 3 | 4 | import { ChatMessageComponent } from './chat-message.component'; 5 | 6 | describe('ChatMessageComponent', () => { 7 | let component: ChatMessageComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [ChatMessageComponent, MarkdownModule.forRoot()], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ChatMessageComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-message/chat-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from '@angular/core'; 2 | import { 3 | ChatRole, 4 | ChatMessage, 5 | } from '../../../modules/+chat/shared/chat.model'; 6 | import { MarkdownComponent } from 'ngx-markdown'; 7 | import { ChatAudioComponent } from '../chat-audio/chat-audio.component'; 8 | import { NgClass } from '@angular/common'; 9 | import { ChatAvatarComponent } from '../chat-avatar/chat-avatar.component'; 10 | import { MessageImageFilePipe } from '../../../pipes/message-file.pipe'; 11 | import { AnnotationPipe } from '../../../pipes/annotation.pipe'; 12 | import { ChatAnnotationsComponent } from '../chat-annotations/chat-annotations.component'; 13 | 14 | @Component({ 15 | selector: 'ai-chat-message', 16 | standalone: true, 17 | templateUrl: './chat-message.component.html', 18 | styleUrl: './chat-message.component.scss', 19 | imports: [ 20 | NgClass, 21 | MarkdownComponent, 22 | ChatAudioComponent, 23 | ChatAvatarComponent, 24 | MessageImageFilePipe, 25 | AnnotationPipe, 26 | ChatAnnotationsComponent, 27 | ], 28 | }) 29 | export class ChatMessageComponent { 30 | @Input() message!: Partial; 31 | @Input() class = ''; 32 | chatRole = ChatRole; 33 | 34 | @HostBinding('class') get getClasses(): string { 35 | return `${this.class} is-${this.message?.role || 'none'}`; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-messages/chat-messages.component.html: -------------------------------------------------------------------------------- 1 |
2 | @for (message of initialMessages.concat(messages); track message.id) { 3 | 4 | 5 | 6 | } @empty { 7 | 8 | } 9 | 10 |
11 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-messages/chat-messages.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | .messages { 4 | position: relative; 5 | display: flex; 6 | gap: $size-5; 7 | flex-direction: column; 8 | max-height: calc(100vh - 136px); 9 | padding: $size-6 $size-4 $size-0; 10 | box-sizing: border-box; 11 | overflow-y: auto; 12 | width: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-messages/chat-messages.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatMessagesComponent } from './chat-messages.component'; 4 | 5 | describe('CardContentComponent', () => { 6 | let component: ChatMessagesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatMessagesComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatMessagesComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-messages/chat-messages.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ElementRef, 5 | EventEmitter, 6 | Input, 7 | OnChanges, 8 | Output, 9 | QueryList, 10 | SimpleChanges, 11 | ViewChildren, 12 | } from '@angular/core'; 13 | import { ChatMessage } from '../../../modules/+chat/shared/chat.model'; 14 | import { ChatMessageComponent } from '../chat-message/chat-message.component'; 15 | import { ChatTypingComponent } from '../chat-typing/chat-typing.component'; 16 | import { ChatContentComponent } from '../chat-content/chat-content.component'; 17 | import { ChatTipsComponent } from '../chat-tips/chat-tips.component'; 18 | 19 | @Component({ 20 | selector: 'ai-chat-messages', 21 | standalone: true, 22 | templateUrl: './chat-messages.component.html', 23 | styleUrl: './chat-messages.component.scss', 24 | imports: [ 25 | ChatMessageComponent, 26 | ChatTypingComponent, 27 | ChatContentComponent, 28 | ChatTipsComponent, 29 | ], 30 | }) 31 | export class ChatMessagesComponent implements AfterViewInit, OnChanges { 32 | @Input() initialMessages: Partial[] = []; 33 | @Input() messages: Partial[] = []; 34 | @Input() isTyping = false; 35 | @Input() tips: string[] = []; 36 | @Output() tipSelected$ = new EventEmitter(); 37 | @ViewChildren('item') item?: QueryList; 38 | 39 | ngAfterViewInit() { 40 | this.scrollDown(); 41 | } 42 | 43 | ngOnChanges(changes: SimpleChanges) { 44 | if (changes['messages']) { 45 | this.scrollDown(); 46 | } 47 | } 48 | 49 | scrollDown(): void { 50 | setTimeout(() => { 51 | const lastChildElement = this.item?.last?.nativeElement; 52 | lastChildElement?.scrollIntoView({ behavior: 'smooth' }); 53 | }, 0); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-status/chat-status.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-status/chat-status.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: block; 5 | position: absolute; 6 | right: -2px; 7 | bottom: 0; 8 | } 9 | 10 | .chat-status { 11 | border-radius: 50%; 12 | width: 8px; 13 | height: 8px; 14 | background-color: var(--color-green-500); 15 | } 16 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-status/chat-status.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatStatusComponent } from './chat-status.component'; 4 | 5 | describe('ChatStatusComponent', () => { 6 | let component: ChatStatusComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatStatusComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatStatusComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-status/chat-status.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-chat-status', 5 | standalone: true, 6 | templateUrl: './chat-status.component.html', 7 | styleUrl: './chat-status.component.scss', 8 | }) 9 | export class ChatStatusComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tip/chat-tip.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tip/chat-tip.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | padding: $size-2 $size-4; 5 | box-shadow: var(--shadow-light); 6 | background-color: var(--color-primary-200); 7 | border-radius: var(--border-radius-medium); 8 | transition: var(--animation-fast); 9 | cursor: pointer; 10 | 11 | &:hover { 12 | box-shadow: var(--shadow-default); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tip/chat-tip.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatTipComponent } from './chat-tip.component'; 4 | 5 | describe('ChatTipComponent', () => { 6 | let component: ChatTipComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatTipComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatTipComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tip/chat-tip.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-chat-tip', 5 | standalone: true, 6 | templateUrl: './chat-tip.component.html', 7 | styleUrl: './chat-tip.component.scss', 8 | }) 9 | export class ChatTipComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tips/chat-tips.component.html: -------------------------------------------------------------------------------- 1 | @for (tip of tips; track $index) { 2 | 3 | {{ tip }} 4 | 5 | } 6 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tips/chat-tips.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | flex-wrap: wrap; 6 | gap: $size-4 $size-2; 7 | } 8 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tips/chat-tips.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatTipsComponent } from './chat-tips.component'; 4 | 5 | describe('ChatTipComponent', () => { 6 | let component: ChatTipsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatTipsComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatTipsComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-tips/chat-tips.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { ChatTipComponent } from '../chat-tip/chat-tip.component'; 3 | 4 | @Component({ 5 | selector: 'ai-chat-tips', 6 | standalone: true, 7 | templateUrl: './chat-tips.component.html', 8 | styleUrl: './chat-tips.component.scss', 9 | imports: [ChatTipComponent], 10 | }) 11 | export class ChatTipsComponent { 12 | @Input() tips: string[] = []; 13 | @Output() tipSelected$ = new EventEmitter(); 14 | } 15 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-trigger/chat-trigger.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-trigger/chat-trigger.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | position: fixed; 5 | bottom: 0; 6 | right: 0; 7 | margin: $size-6; 8 | width: 60px; 9 | height: 60px; 10 | border: 6px solid var(--color-white); 11 | border-radius: 50%; 12 | box-shadow: var(--shadow-default); 13 | background-color: var(--color-grey-900); 14 | background-image: url('/assets/trigger.jpeg'); 15 | background-size: cover; 16 | background-position: center; 17 | transition: var(--animation-fast); 18 | cursor: pointer; 19 | 20 | &:hover { 21 | box-shadow: var(--shadow-default-hover); 22 | transform: scale(1.05); 23 | } 24 | 25 | &.is-animated { 26 | animation: trigger 1.5s infinite; 27 | animation-fill-mode: both; 28 | } 29 | } 30 | 31 | @keyframes trigger { 32 | 0% { 33 | transform: scale(1); 34 | } 35 | 20% { 36 | transform: scale(1.05); 37 | } 38 | 100% { 39 | transform: scale(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-trigger/chat-trigger.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatTriggerComponent } from './chat-trigger.component'; 4 | 5 | describe('ChatTriggerComponent', () => { 6 | let component: ChatTriggerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatTriggerComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatTriggerComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-trigger/chat-trigger.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-chat-trigger', 5 | standalone: true, 6 | templateUrl: './chat-trigger.component.html', 7 | styleUrl: './chat-trigger.component.scss', 8 | }) 9 | export class ChatTriggerComponent { 10 | @HostBinding('class.ai-assistant-toggle') isTrigger = true; 11 | @HostBinding('class.is-animated') @Input() isAnimated = true; 12 | } 13 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-typing/chat-typing.component.html: -------------------------------------------------------------------------------- 1 | @if (isTyping) { 2 |
3 |
4 | 5 | 6 | 7 |
8 |
9 | } 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-typing/chat-typing.component.scss: -------------------------------------------------------------------------------- 1 | $dot-width: 10px; 2 | $speed: 1.5s; 3 | 4 | :hover:empty { 5 | display: none; 6 | } 7 | 8 | .chat-typing__dots { 9 | position: relative; 10 | display: flex; 11 | width: 50px; 12 | height: 8px; 13 | margin: 8px; 14 | 15 | span { 16 | content: ''; 17 | animation: blink $speed infinite; 18 | animation-fill-mode: both; 19 | height: $dot-width; 20 | width: $dot-width; 21 | background: var(--color-grey-500); 22 | position: absolute; 23 | left: 0; 24 | top: 0; 25 | border-radius: 50%; 26 | 27 | &:nth-child(2) { 28 | animation-delay: 0.2s; 29 | margin-left: $dot-width * 1.5; 30 | } 31 | 32 | &:nth-child(3) { 33 | animation-delay: 0.4s; 34 | margin-left: $dot-width * 3; 35 | } 36 | } 37 | } 38 | 39 | @keyframes blink { 40 | 0% { 41 | opacity: 0.1; 42 | } 43 | 20% { 44 | opacity: 1; 45 | } 46 | 100% { 47 | opacity: 0.1; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-typing/chat-typing.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatTypingComponent } from './chat-typing.component'; 4 | 5 | describe('MessageTyping', () => { 6 | let component: ChatTypingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatTypingComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ChatTypingComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/chat/chat-typing/chat-typing.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-chat-typing', 5 | standalone: true, 6 | templateUrl: './chat-typing.component.html', 7 | styleUrl: './chat-typing.component.scss', 8 | }) 9 | export class ChatTypingComponent { 10 | @Input() isTyping = false; 11 | } 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-icon/control-icon.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-icon/control-icon.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | } 6 | 7 | .control-icon { 8 | position: relative; 9 | width: auto; 10 | height: auto; 11 | font-size: 20px; 12 | color: inherit; 13 | z-index: 1; 14 | } 15 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-icon/control-icon.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ControlIconComponent } from './control-icon.component'; 4 | 5 | describe('CardComponent', () => { 6 | let component: ControlIconComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ControlIconComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ControlIconComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-icon/control-icon.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIcon } from '@angular/material/icon'; 3 | 4 | @Component({ 5 | selector: 'ai-control-icon', 6 | standalone: true, 7 | templateUrl: './control-icon.component.html', 8 | styleUrl: './control-icon.component.scss', 9 | imports: [MatIcon], 10 | }) 11 | export class ControlIconComponent {} 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-item/control-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-item/control-item.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | position: relative; 5 | display: flex; 6 | flex-direction: row; 7 | align-items: center; 8 | justify-content: center; 9 | padding: $size-2; 10 | background-color: var(--color-grey-200); 11 | transition: var(--animation-fast); 12 | border-radius: var(--border-radius-default); 13 | width: 48px; 14 | height: 48px; 15 | cursor: pointer; 16 | box-sizing: border-box; 17 | color: var(--color-grey-500); 18 | 19 | &.has-files, 20 | &.is-active, 21 | &:hover { 22 | background-color: var(--color-grey-300); 23 | color: var(--color-grey-900); 24 | } 25 | 26 | &.is-disabled { 27 | pointer-events: none; 28 | opacity: 0.5; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-item/control-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ControlItemComponent } from './control-item.component'; 4 | 5 | describe('CardComponent', () => { 6 | let component: ControlItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ControlItemComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ControlItemComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/control-item/control-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-control-item', 5 | standalone: true, 6 | templateUrl: './control-item.component.html', 7 | styleUrl: './control-item.component.scss', 8 | }) 9 | export class ControlItemComponent { 10 | @HostBinding('class.is-disabled') @Input() isDisabled = false; 11 | } 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/controls/controls.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/controls/controls.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | gap: $size-1; 6 | 7 | &:empty { 8 | display: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/controls/controls.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ControlsComponent } from './controls.component'; 4 | 5 | describe('CardComponent', () => { 6 | let component: ControlsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ControlsComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ControlsComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/controls/controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-controls', 5 | standalone: true, 6 | templateUrl: './controls.component.html', 7 | styleUrl: './controls.component.scss', 8 | }) 9 | export class ControlsComponent {} 10 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/files/files.component.html: -------------------------------------------------------------------------------- 1 | 7 | attach_file 8 | 9 | @if (files().length) { 10 | 11 | 12 | {{ files().length }} 13 | 14 | close 15 | 16 | } 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/files/files.component.scss: -------------------------------------------------------------------------------- 1 | .files__input { 2 | display: none; 3 | } 4 | 5 | .files__counter { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | background-color: var(--color-red-500); 10 | border-radius: 50%; 11 | min-width: 20px; 12 | min-height: 20px; 13 | text-align: center; 14 | font-size: 10px; 15 | color: var(--color-white); 16 | position: absolute; 17 | right: 2px; 18 | top: 2px; 19 | z-index: 1; 20 | 21 | &:hover { 22 | .files__number { 23 | display: none; 24 | } 25 | 26 | .files__clear { 27 | display: block; 28 | } 29 | } 30 | } 31 | 32 | .files__clear { 33 | display: none; 34 | font-size: 12px; 35 | height: 12px; 36 | width: 12px; 37 | } 38 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/files/files.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FilesComponent } from './files.component'; 4 | 5 | describe('FilesComponent', () => { 6 | let component: FilesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [FilesComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(FilesComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/files/files.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core'; 2 | import { MatIcon } from '@angular/material/icon'; 3 | import { AiFilesDirective } from './files.directive'; 4 | import { toSignal } from '@angular/core/rxjs-interop'; 5 | import { FilesService } from './files.service'; 6 | import { ControlItemComponent } from '../control-item/control-item.component'; 7 | import { ControlIconComponent } from '../control-icon/control-icon.component'; 8 | 9 | @Component({ 10 | selector: 'ai-files', 11 | standalone: true, 12 | imports: [ 13 | MatIcon, 14 | AiFilesDirective, 15 | ControlItemComponent, 16 | ControlIconComponent, 17 | ], 18 | templateUrl: './files.component.html', 19 | styleUrl: './files.component.scss', 20 | }) 21 | export class FilesComponent { 22 | @ViewChild('input') input!: ElementRef; 23 | @Input() isDisabled = false; 24 | files = toSignal(this.fileService.files$, { initialValue: [] }); 25 | 26 | constructor(private readonly fileService: FilesService) {} 27 | 28 | addFiles(files: FileList) { 29 | this.fileService.add(files); 30 | } 31 | 32 | onFileChange(event: Event) { 33 | const input = event.target as HTMLInputElement; 34 | this.addFiles(input.files as FileList); 35 | } 36 | 37 | clear(event: Event): void { 38 | event.preventDefault(); 39 | event.stopPropagation(); 40 | this.input.nativeElement.files = null; 41 | this.input.nativeElement.value = ''; 42 | this.fileService.clear(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/files/files.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | HostListener, 4 | HostBinding, 5 | Output, 6 | EventEmitter, 7 | Input, 8 | } from '@angular/core'; 9 | import { MessageContent } from 'openai/resources/beta/threads/messages'; 10 | 11 | @Directive({ 12 | standalone: true, 13 | selector: '[aiFiles]', 14 | }) 15 | export class AiFilesDirective { 16 | @Output() drop$: EventEmitter = new EventEmitter(); 17 | @Input() files: Array = []; 18 | event = 'init'; 19 | 20 | @HostBinding('class') get getClasses(): string { 21 | return `drag-drop--${this.event} ${ 22 | this.files.length ? 'has-files' : 'no-files' 23 | }`; 24 | } 25 | 26 | @HostListener('dragover', ['$event']) public onDragOver( 27 | event: DragEvent, 28 | ): void { 29 | event.preventDefault(); 30 | event.stopPropagation(); 31 | this.event = 'dragleave'; 32 | } 33 | 34 | @HostListener('drag', ['$event']) public onDragEnd(event: DragEvent): void { 35 | this.initEvent(event); 36 | } 37 | 38 | @HostListener('dragleave', ['$event']) public onDragLeave( 39 | event: DragEvent, 40 | ): void { 41 | this.initEvent(event); 42 | } 43 | 44 | @HostListener('drop', ['$event']) public onDrop(event: DragEvent): void { 45 | this.initEvent(event); 46 | const files = event.dataTransfer?.files; 47 | 48 | if (!files) { 49 | return; 50 | } 51 | 52 | this.drop$.emit(files); 53 | } 54 | 55 | initEvent(event: DragEvent): void { 56 | event.preventDefault(); 57 | event.stopPropagation(); 58 | this.event = 'init'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/files/files.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class FilesService { 6 | files$ = new BehaviorSubject([]); 7 | 8 | add(files: FileList) { 9 | const convertedFiles = Object.keys(files).map( 10 | key => files[key as unknown as number], 11 | ); 12 | const updatedFiles = [ 13 | ...this.files$.value, 14 | ...convertedFiles, 15 | ]; 16 | 17 | this.files$.next(updatedFiles); 18 | } 19 | 20 | delete(index: number): void { 21 | const updatedFiles = this.files$.value.splice(index, 1); 22 | this.files$.next(updatedFiles); 23 | } 24 | 25 | clear(): void { 26 | this.files$.next([]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './files/files.directive'; 2 | export * from './files/files.component'; 3 | export * from './files/files.service'; 4 | export * from './recorder/recorder.component'; 5 | export * from './input/input.component'; 6 | export * from './controls/controls.component'; 7 | export * from './control-item/control-item.component'; 8 | export * from './control-icon/control-icon.component'; 9 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/input/input.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/input/input.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | width: 100%; 4 | } 5 | 6 | .input__suffix { 7 | margin-right: 4px; 8 | 9 | &.mat-mdc-button-disabled { 10 | opacity: 0.5; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/input/input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { InputComponent } from './input.component'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | describe('CardComponent', () => { 7 | let component: InputComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [InputComponent, BrowserAnimationsModule], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(InputComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/input/input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { MatFormField } from '@angular/material/form-field'; 3 | import { MatInputModule } from '@angular/material/input'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | 8 | @Component({ 9 | selector: 'ai-input', 10 | standalone: true, 11 | templateUrl: './input.component.html', 12 | styleUrl: './input.component.scss', 13 | imports: [ 14 | MatFormField, 15 | MatInputModule, 16 | FormsModule, 17 | MatIconModule, 18 | MatButtonModule, 19 | ], 20 | }) 21 | export class InputComponent { 22 | @Input() isDisabled = false; 23 | @Output() send$ = new EventEmitter(); 24 | content = ''; 25 | 26 | send(content: string): void { 27 | if (!content?.trim()) { 28 | return; 29 | } 30 | 31 | this.send$.emit(content); 32 | this.content = ''; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/message-content/message-content.component.html: -------------------------------------------------------------------------------- 1 | 7 | image 8 | 9 | @if (imageContentList$().length) { 10 | 11 | 12 | {{ imageContentList$().length }} 13 | 14 | close 15 | 16 | } 17 | 18 | 25 | 26 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/message-content/message-content.component.scss: -------------------------------------------------------------------------------- 1 | .files__input { 2 | display: none; 3 | } 4 | 5 | .files__counter { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | background-color: var(--color-red-500); 10 | border-radius: 50%; 11 | min-width: 20px; 12 | min-height: 20px; 13 | text-align: center; 14 | font-size: 10px; 15 | color: var(--color-white); 16 | position: absolute; 17 | right: 2px; 18 | top: 2px; 19 | z-index: 1; 20 | 21 | &:hover { 22 | .files__number { 23 | display: none; 24 | } 25 | 26 | .files__clear { 27 | display: block; 28 | } 29 | } 30 | } 31 | 32 | .files__clear { 33 | display: none; 34 | font-size: 12px; 35 | height: 12px; 36 | width: 12px; 37 | } 38 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/message-content/message-content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageContentComponent } from './message-content.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | 6 | describe('MessageContentComponent', () => { 7 | let component: MessageContentComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [MessageContentComponent, HttpClientTestingModule], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MessageContentComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/message-content/message-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core'; 2 | import { MatIcon } from '@angular/material/icon'; 3 | import { toSignal } from '@angular/core/rxjs-interop'; 4 | import { ControlItemComponent } from '../control-item/control-item.component'; 5 | import { ControlIconComponent } from '../control-icon/control-icon.component'; 6 | import { AiFilesDirective } from '../files/files.directive'; 7 | import { MessageContentService } from './message-content.service'; 8 | 9 | @Component({ 10 | selector: 'ai-message-content', 11 | standalone: true, 12 | imports: [ 13 | MatIcon, 14 | AiFilesDirective, 15 | ControlItemComponent, 16 | ControlIconComponent, 17 | ], 18 | templateUrl: './message-content.component.html', 19 | styleUrl: './message-content.component.scss', 20 | }) 21 | export class MessageContentComponent { 22 | @ViewChild('input') input!: ElementRef; 23 | @Input() isDisabled = false; 24 | imageContentList$ = toSignal(this.messageContentService.data$, { 25 | initialValue: [], 26 | }); 27 | 28 | constructor(private readonly messageContentService: MessageContentService) {} 29 | 30 | addFiles(files: FileList) { 31 | this.messageContentService.add(files); 32 | } 33 | 34 | onFileChange(event: Event) { 35 | const input = event.target as HTMLInputElement; 36 | this.addFiles(input.files as FileList); 37 | } 38 | 39 | clear(event: Event): void { 40 | event.preventDefault(); 41 | event.stopPropagation(); 42 | this.input.nativeElement.files = null; 43 | this.input.nativeElement.value = ''; 44 | this.messageContentService.clear(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/message-content/message-content.helpers.ts: -------------------------------------------------------------------------------- 1 | import { TextContentBlock } from 'openai/resources/beta/threads/messages'; 2 | import { ImageFileContentBlock } from 'openai/resources/beta/threads/messages'; 3 | 4 | export function isTextContentBlock(item?: { 5 | type?: string; 6 | }): item is TextContentBlock { 7 | return item?.type === 'text'; 8 | } 9 | 10 | export function isImageFileContentBlock(item?: { 11 | type?: string; 12 | }): item is ImageFileContentBlock { 13 | return item?.type === 'image_file'; 14 | } 15 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/message-content/message-content.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service'; 4 | import { OpenAiFile } from '@boldare/openai-assistant'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class MessageContentService { 8 | data$ = new BehaviorSubject([]); 9 | 10 | constructor(private readonly chatClientService: ChatClientService) {} 11 | 12 | add(files: FileList) { 13 | const convertedFiles = Object.keys(files).map( 14 | key => files[key as unknown as number], 15 | ); 16 | const updatedFiles = [...this.data$.value, ...convertedFiles]; 17 | this.data$.next(updatedFiles); 18 | } 19 | 20 | delete(index: number): void { 21 | const updatedFiles = this.data$.value.splice(index, 1); 22 | this.data$.next(updatedFiles); 23 | } 24 | 25 | clear(): void { 26 | this.data$.next([]); 27 | } 28 | 29 | async sendFiles(): Promise { 30 | const files = this.data$.value; 31 | 32 | if (!files.length) { 33 | return []; 34 | } 35 | 36 | const uploadedFilesResponse = await this.chatClientService.uploadFiles({ 37 | files, 38 | }); 39 | this.clear(); 40 | 41 | return uploadedFilesResponse.files || []; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/recorder/recorder.component.html: -------------------------------------------------------------------------------- 1 | 5 | mic 6 |
7 |
8 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/recorder/recorder.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | .is-active { 4 | .recorder__circle { 5 | display: block; 6 | background-color: var(--color-red-500); 7 | animation: ease pulse 1.5s infinite; 8 | } 9 | 10 | .recorder__icon { 11 | color: var(--color-white); 12 | } 13 | } 14 | 15 | .recorder__circle { 16 | display: none; 17 | position: absolute; 18 | z-index: 0; 19 | background-color: #e83333; 20 | width: 70%; 21 | height: 70%; 22 | border-radius: 50%; 23 | transition: all 0.2s ease-in-out; 24 | } 25 | 26 | @keyframes pulse { 27 | 0% { 28 | background-color: red; 29 | transform: scale(1); 30 | } 31 | 50% { 32 | transform: scale(1.15); 33 | } 34 | 100% { 35 | background-color: red; 36 | transform: scale(1); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/recorder/recorder.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RecorderComponent } from './recorder.component'; 4 | 5 | describe('ChatRecorderComponent', () => { 6 | let component: RecorderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [RecorderComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(RecorderComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/controls/recorder/recordrtc.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'recordrtc' { 2 | export class StereoAudioRecorder { 3 | constructor(stream: unknown, options: unknown); 4 | record(): void; 5 | stop(callback: (blob: Blob) => void): void; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/spinner/spinner.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/spinner/spinner.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | bottom: 0; 8 | left: 0; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | background-color: var(--color-white); 13 | z-index: 2; 14 | opacity: 0; 15 | transition: ease-out opacity 0.3s; 16 | pointer-events: none; 17 | border-radius: var(--border-radius-medium); 18 | 19 | &.is-active { 20 | opacity: 1; 21 | pointer-events: all; 22 | } 23 | } 24 | 25 | .spinner { 26 | width: 88px; 27 | height: 88px; 28 | border-radius: 50%; 29 | display: inline-block; 30 | position: relative; 31 | border: 10px solid; 32 | border-color: rgba(168, 135, 92, 0.15) rgba(168, 135, 92, 0.25) 33 | rgba(168, 135, 92, 0.35) rgba(168, 135, 92, 0.5); 34 | box-sizing: border-box; 35 | animation: rotation 1s linear infinite; 36 | } 37 | 38 | @keyframes rotation { 39 | 0% { 40 | transform: rotate(0deg); 41 | } 42 | 100% { 43 | transform: rotate(360deg); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/spinner/spinner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SpinnerComponent } from './spinner.component'; 4 | 5 | describe('SpinnerComponent', () => { 6 | let component: SpinnerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SpinnerComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(SpinnerComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/spa/src/app/components/spinner/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ai-spinner', 5 | standalone: true, 6 | templateUrl: './spinner.component.html', 7 | styleUrl: './spinner.component.scss', 8 | }) 9 | export class SpinnerComponent { 10 | @HostBinding('class.is-active') @Input() isActive = false; 11 | } 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/chat.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { ChatComponent } from './containers/chat/chat.component'; 3 | 4 | export const routes: Routes = [ 5 | { 6 | path: '', 7 | component: ChatComponent, 8 | children: [ 9 | { 10 | path: '', 11 | loadComponent: () => 12 | import('./containers/chat-home/chat-home.component').then( 13 | mod => mod.ChatHomeComponent, 14 | ), 15 | }, 16 | { 17 | path: 'chat', 18 | loadComponent: () => 19 | import('./containers/chat-cloud/chat-cloud.component').then( 20 | mod => mod.ChatCloudComponent, 21 | ), 22 | }, 23 | { 24 | path: 'integration', 25 | loadComponent: () => 26 | import( 27 | './containers/chat-integration/chat-integration.component' 28 | ).then(mod => mod.ChatIntegrationComponent), 29 | }, 30 | ], 31 | }, 32 | { 33 | path: 'chat/iframe', 34 | loadComponent: () => 35 | import('./containers/chat-iframe/chat-iframe.component').then( 36 | mod => mod.ChatIframeComponent, 37 | ), 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-cloud/chat-cloud.component.html: -------------------------------------------------------------------------------- 1 |
2 | The following are some examples of what you can do with the demo: 3 | 4 |
    5 |
  • Speak about the weather (eg. What's the weather in London?)
  • 6 |
  • 7 | Speak about the exchange rate (eg. What's the exchange rate for USD?) 8 |
  • 9 |
  • Speak about the Pokemon (eg. Show me the stats of Pikachu)
  • 10 |
11 |
12 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-cloud/chat-cloud.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | max-width: 600px; 3 | margin-top: 10px; 4 | color: var(--color-grey-600); 5 | font-size: 15px; 6 | letter-spacing: 0.03em; 7 | text-align: center; 8 | line-height: 1.6; 9 | font-weight: 300; 10 | } 11 | 12 | .chat-home__examples { 13 | margin-top: 10px; 14 | font-size: 15px; 15 | text-align: left; 16 | } 17 | 18 | .chat-home__list { 19 | text-align: left; 20 | margin-left: 12px; 21 | 22 | > li { 23 | padding-left: 8px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-cloud/chat-cloud.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { MarkdownModule } from 'ngx-markdown'; 4 | 5 | import { ChatCloudComponent } from './chat-cloud.component'; 6 | import { AnnotationPipe } from '../../../../pipes/annotation.pipe'; 7 | 8 | describe('ChatHomeComponent', () => { 9 | let component: ChatCloudComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | imports: [ 15 | ChatCloudComponent, 16 | HttpClientTestingModule, 17 | MarkdownModule.forRoot(), 18 | ], 19 | providers: [AnnotationPipe], 20 | }).compileComponents(); 21 | 22 | fixture = TestBed.createComponent(ChatCloudComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-cloud/chat-cloud.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AssistantIframe } from '@boldare/ai-embedded'; 3 | import { ChatService } from '../../shared/chat.service'; 4 | import { environment } from '../../../../../environments/environment'; 5 | 6 | @Component({ 7 | selector: 'ai-chat-home', 8 | standalone: true, 9 | templateUrl: './chat-cloud.component.html', 10 | styleUrl: './chat-cloud.component.scss', 11 | }) 12 | export class ChatCloudComponent { 13 | constructor(private readonly chatService: ChatService) { 14 | if (environment.env === 'prod') { 15 | this.chatService.loadScript(); 16 | } else { 17 | new AssistantIframe().init(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: relative; 4 | max-width: 800px; 5 | width: 100%; 6 | font-size: 14px; 7 | z-index: 1; 8 | } 9 | 10 | .chat-home__iframe { 11 | display: block; 12 | height: auto; 13 | max-height: inherit; 14 | } 15 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatHomeComponent } from './chat-home.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { MarkdownModule } from 'ngx-markdown'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { AnnotationPipe } from '../../../../pipes/annotation.pipe'; 8 | 9 | describe('ChatHomeComponent', () => { 10 | let component: ChatHomeComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async () => { 14 | await TestBed.configureTestingModule({ 15 | imports: [ 16 | ChatHomeComponent, 17 | HttpClientTestingModule, 18 | BrowserAnimationsModule, 19 | MarkdownModule.forRoot(), 20 | ], 21 | providers: [AnnotationPipe], 22 | }).compileComponents(); 23 | 24 | fixture = TestBed.createComponent(ChatHomeComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-home/chat-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ChatIframeComponent } from '../chat-iframe/chat-iframe.component'; 3 | 4 | @Component({ 5 | selector: 'ai-chat-home', 6 | standalone: true, 7 | templateUrl: './chat-home.component.html', 8 | styleUrl: './chat-home.component.scss', 9 | imports: [ChatIframeComponent], 10 | }) 11 | export class ChatHomeComponent {} 12 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | @if (isConfigEnabled && !threadId()) { 13 | 14 | } @else { 15 | 22 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | height: 100vh; 4 | max-height: 600px; 5 | background-color: var(--color-white); 6 | border-radius: 0 0 var(--border-radius-medium) var(--border-radius-medium); 7 | 8 | &.chat-home__iframe .chat__content, 9 | &.chat-home__iframe ::ng-deep .messages { 10 | min-height: 280px; 11 | height: calc(100vh - 300px); 12 | max-height: 500px; 13 | overflow: auto; 14 | } 15 | } 16 | 17 | .chat { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .chat__content { 23 | flex: 1 1 100%; 24 | } 25 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-iframe/chat-iframe.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatIframeComponent } from './chat-iframe.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { AnnotationPipe } from '../../../../pipes/annotation.pipe'; 8 | 9 | describe('ChatIframeComponent', () => { 10 | let component: ChatIframeComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async () => { 14 | await TestBed.configureTestingModule({ 15 | imports: [ 16 | HttpClientTestingModule, 17 | BrowserAnimationsModule, 18 | ChatIframeComponent, 19 | RouterTestingModule, 20 | ], 21 | providers: [AnnotationPipe], 22 | }).compileComponents(); 23 | 24 | fixture = TestBed.createComponent(ChatIframeComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-integration/chat-integration.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | flex-direction: column; 6 | gap: $size-2; 7 | max-width: 680px; 8 | font-size: 15px; 9 | margin: 0 auto; 10 | padding: 40px 10px; 11 | color: var(--color-grey-500); 12 | } 13 | 14 | .section { 15 | margin-bottom: 20px; 16 | } 17 | 18 | p.paragraph { 19 | margin-bottom: 12px; 20 | 21 | &:last-child { 22 | margin-bottom: 0; 23 | } 24 | } 25 | 26 | h1 { 27 | color: var(--color-grey-600); 28 | } 29 | 30 | h2 { 31 | color: var(--color-grey-600); 32 | margin-bottom: 10px; 33 | font-weight: 400; 34 | font-size: 18px; 35 | } 36 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-integration/chat-integration.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatIntegrationComponent } from './chat-integration.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { MarkdownModule } from 'ngx-markdown'; 6 | 7 | describe('ChatIntegrationComponent', () => { 8 | let component: ChatIntegrationComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [ 14 | ChatIntegrationComponent, 15 | HttpClientTestingModule, 16 | MarkdownModule.forRoot(), 17 | ], 18 | }).compileComponents(); 19 | 20 | fixture = TestBed.createComponent(ChatIntegrationComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat-integration/chat-integration.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AsyncPipe } from '@angular/common'; 3 | import { environment } from '../../../../../environments/environment'; 4 | import { MarkdownComponent, MarkdownPipe } from 'ngx-markdown'; 5 | 6 | @Component({ 7 | selector: 'ai-chat-integration', 8 | standalone: true, 9 | templateUrl: './chat-integration.component.html', 10 | styleUrl: './chat-integration.component.scss', 11 | imports: [MarkdownComponent, MarkdownPipe, AsyncPipe], 12 | }) 13 | export class ChatIntegrationComponent { 14 | scriptMarkdown = `\`\`\`html 15 | 20 | \`\`\``; 21 | 22 | scriptDataAttrMarkdown = `\`\`\`javascript 23 | data-chat-initial="true" 24 | \`\`\``; 25 | 26 | manualInitialization = `\`\`\`javascript 27 | new AssistantIframe({ 28 | url: \`${environment.appUrl}/chat/iframe\` 29 | }).init(); 30 | \`\`\``; 31 | } 32 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat/chat.component.html: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | Powered by digital product creators at 37 | 42 | 43 | 44 |
45 | A NestJS library for building efficient, scalable, and quick solutions based 46 | on the OpenAI Assistant API (chatbots) 🤖 🚀 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat/chat.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | :host { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | max-width: 900px; 9 | margin: 0 auto; 10 | padding: 50px 20px 20px; 11 | font-size: 16px; 12 | } 13 | 14 | .chat__logo { 15 | width: 100%; 16 | cursor: pointer; 17 | margin: 0 auto 20px; 18 | max-width: 200px; 19 | } 20 | 21 | .chat__nav { 22 | display: flex; 23 | flex-wrap: wrap; 24 | gap: 10px; 25 | margin-bottom: 20px; 26 | text-align: center; 27 | justify-content: center; 28 | font-size: 13px; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | right: 0; 33 | padding: 6px; 34 | background-color: var(--color-blue-500); 35 | color: var(--color-white); 36 | 37 | @include min-screen($tablet) { 38 | font-size: 14px; 39 | gap: 22px; 40 | } 41 | } 42 | 43 | .chat__link, 44 | .chat__link:visited { 45 | color: var(--color-white); 46 | text-decoration: none; 47 | 48 | &:hover { 49 | color: var(--color-grey-300); 50 | } 51 | } 52 | 53 | .chat__header { 54 | display: flex; 55 | flex-direction: column; 56 | max-width: 240px; 57 | width: 100%; 58 | text-decoration: none; 59 | color: var(--color-black); 60 | text-align: center; 61 | gap: 10px; 62 | font-size: 13px; 63 | font-weight: 400; 64 | } 65 | 66 | .chat__description { 67 | font-size: 13px; 68 | max-width: 460px; 69 | text-align: center; 70 | color: var(--color-grey-600); 71 | font-weight: 100; 72 | margin-bottom: 20px; 73 | 74 | @include min-screen($tablet) { 75 | font-size: 15px; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat/chat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatComponent } from './chat.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { MarkdownModule } from 'ngx-markdown'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | 8 | describe('ChatComponent', () => { 9 | let component: ChatComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | imports: [ 15 | ChatComponent, 16 | HttpClientTestingModule, 17 | RouterTestingModule, 18 | MarkdownModule.forRoot(), 19 | ], 20 | }).compileComponents(); 21 | 22 | fixture = TestBed.createComponent(ChatComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/containers/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { 3 | Router, 4 | RouterLink, 5 | RouterModule, 6 | RouterOutlet, 7 | } from '@angular/router'; 8 | import { ChatIframeWrapperComponent } from '../../../../components/chat/chat-iframe-wrapper/chat-iframe-wrapper.component'; 9 | import { ChatIframeComponent } from '../chat-iframe/chat-iframe.component'; 10 | 11 | @Component({ 12 | selector: 'ai-chat', 13 | standalone: true, 14 | templateUrl: './chat.component.html', 15 | styleUrl: './chat.component.scss', 16 | imports: [ 17 | RouterOutlet, 18 | RouterLink, 19 | ChatIframeComponent, 20 | ChatIframeWrapperComponent, 21 | RouterModule, 22 | ], 23 | }) 24 | export class ChatComponent implements OnInit { 25 | constructor(private readonly router: Router) {} 26 | 27 | ngOnInit(): void { 28 | window.onmessage = event => { 29 | if (event.data == 'changeView') { 30 | window.location.href = this.router.url === '/chat' ? '/' : '/chat'; 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/chat-client.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { lastValueFrom, Observable } from 'rxjs'; 4 | import { environment } from '../../../../environments/environment'; 5 | import { AudioResponse } from './chat.model'; 6 | import { 7 | ChatAudio, 8 | ChatAudioResponse, 9 | PostSpeechDto, 10 | UploadFilesPayload, 11 | UploadFilesResponseDto, 12 | } from '@boldare/openai-assistant'; 13 | import { FileObject } from 'openai/resources'; 14 | 15 | @Injectable({ providedIn: 'root' }) 16 | export class ChatClientService { 17 | apiUrl = `${environment.apiUrl}/assistant`; 18 | 19 | constructor(private readonly httpClient: HttpClient) {} 20 | 21 | transcription(payload: ChatAudio): Observable { 22 | const formData = new FormData(); 23 | 24 | formData.append('file', payload.file); 25 | 26 | return this.httpClient.post( 27 | `${this.apiUrl}/ai/transcription`, 28 | formData, 29 | ); 30 | } 31 | 32 | speech(payload: PostSpeechDto): Observable { 33 | return this.httpClient.post( 34 | `${this.apiUrl}/ai/speech`, 35 | payload, 36 | ); 37 | } 38 | 39 | async uploadFiles( 40 | payload: UploadFilesPayload, 41 | ): Promise { 42 | const formData = new FormData(); 43 | 44 | payload.files.forEach(file => formData.append('files', file)); 45 | 46 | return await lastValueFrom( 47 | this.httpClient.post( 48 | `${this.apiUrl}/files`, 49 | formData, 50 | ), 51 | ); 52 | } 53 | 54 | retriveFile(fileId: string): Observable { 55 | return this.httpClient.get( 56 | `${this.apiUrl}/files/retrive/${fileId}`, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/chat-files.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FilesService } from '../../../components/controls'; 3 | import { ChatClientService } from './chat-client.service'; 4 | import { OpenAiFile } from '@boldare/openai-assistant'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class ChatFilesService { 8 | constructor( 9 | private readonly chatClientService: ChatClientService, 10 | private readonly filesService: FilesService, 11 | ) {} 12 | 13 | async sendFiles(): Promise { 14 | const files = this.filesService.files$.value; 15 | 16 | if (!files.length) { 17 | return []; 18 | } 19 | 20 | const uploadedFilesResponse = await this.chatClientService.uploadFiles({ 21 | files, 22 | }); 23 | this.filesService.clear(); 24 | 25 | return uploadedFilesResponse.files || []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/chat-gateway.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ChatEvents } from './chat.model'; 3 | import io from 'socket.io-client'; 4 | import { 5 | ChatCallDto, 6 | MessageWithAnnotations, 7 | TextCreatedPayload, 8 | TextDeltaPayload, 9 | TextDonePayload, 10 | } from '@boldare/openai-assistant'; 11 | import { Observable } from 'rxjs'; 12 | import { environment } from '../../../../environments/environment'; 13 | 14 | @Injectable({ providedIn: 'root' }) 15 | export class ChatGatewayService { 16 | private socket = io(environment.websocketUrl); 17 | 18 | watchEvent(event: ChatEvents): Observable { 19 | return new Observable(observer => { 20 | this.socket.on(event, data => observer.next(data)); 21 | return () => this.socket.disconnect(); 22 | }); 23 | } 24 | 25 | callStart(payload: ChatCallDto): void { 26 | this.socket.emit(ChatEvents.CallStart, payload); 27 | } 28 | 29 | callDone(): Observable { 30 | return this.watchEvent(ChatEvents.CallDone); 31 | } 32 | 33 | textCreated(): Observable { 34 | return this.watchEvent(ChatEvents.TextCreated); 35 | } 36 | 37 | textDelta(): Observable { 38 | return this.watchEvent(ChatEvents.TextDelta); 39 | } 40 | 41 | textDone(): Observable> { 42 | return this.watchEvent(ChatEvents.TextDone); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/chat.helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImageFileContentBlock, 3 | MessageContent, 4 | MessageCreateParams, 5 | } from 'openai/resources/beta/threads'; 6 | import { TextContentBlock } from 'openai/resources/beta/threads/messages'; 7 | import { ChatMessage, ChatRole } from './chat.model'; 8 | import { CodeInterpreterTool, FileSearchTool } from 'openai/resources/beta'; 9 | 10 | export const textContentBlock = (content: string): TextContentBlock => ({ 11 | type: 'text', 12 | text: { 13 | value: content, 14 | annotations: [], 15 | }, 16 | }); 17 | 18 | export const imageFileContentBlock = ( 19 | fileId: string, 20 | ): ImageFileContentBlock => ({ 21 | type: 'image_file', 22 | image_file: { 23 | file_id: fileId, 24 | }, 25 | }); 26 | 27 | export const messageAttachment = ( 28 | fileId: string, 29 | tools: Array = [ 30 | { type: 'code_interpreter' }, 31 | ], 32 | ): MessageCreateParams.Attachment => ({ 33 | file_id: fileId, 34 | tools, 35 | }); 36 | 37 | export const messageContentBlock = ( 38 | content: MessageContent[], 39 | role: ChatRole, 40 | ): Partial => ({ 41 | content, 42 | role, 43 | }); 44 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/chat.model.ts: -------------------------------------------------------------------------------- 1 | import { AnnotationData } from '@boldare/openai-assistant'; 2 | import { Message } from 'openai/resources/beta/threads'; 3 | 4 | export interface AudioResponse { 5 | content: string; 6 | } 7 | 8 | export enum ChatRole { 9 | User = 'user', 10 | Assistant = 'assistant', 11 | } 12 | 13 | export interface ChatMessage extends Message { 14 | annotations?: AnnotationData[]; 15 | role: ChatRole; 16 | } 17 | 18 | export enum ChatEvents { 19 | CallStart = 'callStart', 20 | CallDone = 'callDone', 21 | MessageCreated = 'messageCreated', 22 | MessageDelta = 'messageDelta', 23 | MessageDone = 'messageDone', 24 | TextCreated = 'textCreated', 25 | TextDelta = 'textDelta', 26 | TextDone = 'textDone', 27 | ImageFileDone = 'imageFileDone', 28 | ToolCallCreated = 'toolCallCreated', 29 | ToolCallDelta = 'toolCallDelta', 30 | ToolCallDone = 'toolCallDone', 31 | RunStepCreated = 'runStepCreated', 32 | RunStepDelta = 'runStepDelta', 33 | RunStepDone = 'runStepDone', 34 | } 35 | 36 | export enum ChatMessageStatus { 37 | Invisible = 'invisible', 38 | } 39 | 40 | export enum SpeechVoice { 41 | Alloy = 'alloy', 42 | Echo = 'echo', 43 | Fable = 'fable', 44 | Onyx = 'onyx', 45 | Nova = 'nova', 46 | Shimmer = 'shimmer', 47 | } 48 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/thread-client.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { 4 | CreateThreadDto, 5 | GetThreadResponseDto, 6 | } from '@boldare/openai-assistant'; 7 | import { Observable } from 'rxjs'; 8 | import { environment } from '../../../../environments/environment'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class ThreadClientService { 12 | constructor(private readonly httpClient: HttpClient) {} 13 | 14 | postThread(payload: CreateThreadDto = {}): Observable { 15 | return this.httpClient.post( 16 | `${environment.apiUrl}/assistant/threads`, 17 | payload, 18 | ); 19 | } 20 | 21 | getThread(id: string): Observable { 22 | return this.httpClient.get( 23 | `${environment.apiUrl}/assistant/threads/${id}`, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+chat/shared/thread.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | BehaviorSubject, 4 | catchError, 5 | Observable, 6 | Subject, 7 | take, 8 | tap, 9 | } from 'rxjs'; 10 | import { environment } from '../../../../environments/environment'; 11 | import { ThreadClientService } from './thread-client.service'; 12 | import { ConfigurationFormService } from '../../+configuration/shared/configuration-form.service'; 13 | import { GetThreadResponseDto } from '@boldare/openai-assistant'; 14 | 15 | @Injectable({ providedIn: 'root' }) 16 | export class ThreadService { 17 | key = 'threadId'; 18 | initialThreadId = environment.isThreadMemorized 19 | ? localStorage.getItem(this.key) || '' 20 | : ''; 21 | threadId$ = new BehaviorSubject(this.initialThreadId); 22 | clear$ = new Subject(); 23 | 24 | constructor( 25 | private readonly threadClientService: ThreadClientService, 26 | private readonly configurationFormService: ConfigurationFormService, 27 | ) {} 28 | 29 | start(): Observable { 30 | const messages = this.configurationFormService.getInitialThreadMessages(); 31 | 32 | return this.threadClientService 33 | .postThread(environment.isConfigEnabled ? messages : {}) 34 | .pipe( 35 | take(1), 36 | tap(({ id }) => this.saveId(id)), 37 | ); 38 | } 39 | 40 | saveId(id: string): void { 41 | localStorage.setItem(this.key, id); 42 | this.threadId$.next(id); 43 | } 44 | 45 | clear(): void { 46 | this.threadId$.next(''); 47 | this.clear$.next(true); 48 | 49 | localStorage.removeItem(this.key); 50 | } 51 | 52 | getThread(id: string): Observable { 53 | return this.threadClientService.getThread(id).pipe( 54 | take(1), 55 | catchError(() => this.start()), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/components/configuration-form/configuration-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | First name 5 | 6 | 7 |
8 | 9 | 10 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/components/configuration-form/configuration-form.component.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | .chat-form__content { 4 | position: relative; 5 | display: flex; 6 | flex: 1 1 100%; 7 | gap: $size-5; 8 | flex-direction: column; 9 | padding: $size-4 $size-4 $size-0; 10 | box-sizing: border-box; 11 | max-height: calc(100vh - 136px); 12 | overflow-y: auto; 13 | width: 100%; 14 | } 15 | 16 | .chat-form { 17 | display: flex; 18 | flex-direction: column; 19 | height: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/components/configuration-form/configuration-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfigurationFormComponent } from './configuration-form.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | describe('ConfigurationFormComponent', () => { 8 | let component: ConfigurationFormComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [ 14 | ConfigurationFormComponent, 15 | HttpClientTestingModule, 16 | BrowserAnimationsModule, 17 | ], 18 | }).compileComponents(); 19 | 20 | fixture = TestBed.createComponent(ConfigurationFormComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/components/configuration-form/configuration-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { ThreadService } from '../../../+chat/shared/thread.service'; 4 | import { take } from 'rxjs'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | import { MatSelectModule } from '@angular/material/select'; 9 | import { ConfigurationFormService } from '../../shared/configuration-form.service'; 10 | import { ConfigurationFormValue } from '../../shared/configuration.model'; 11 | import { CardFooterComponent } from '../../../../components/cards'; 12 | import { voices } from '../../shared/configuration.helpers'; 13 | 14 | @Component({ 15 | selector: 'ai-configuration-form', 16 | standalone: true, 17 | imports: [ 18 | MatButtonModule, 19 | MatFormFieldModule, 20 | MatInputModule, 21 | ReactiveFormsModule, 22 | MatSelectModule, 23 | CardFooterComponent, 24 | ], 25 | templateUrl: './configuration-form.component.html', 26 | styleUrl: './configuration-form.component.scss', 27 | }) 28 | export class ConfigurationFormComponent { 29 | voices = voices; 30 | form = this.configurationFormService.form; 31 | @Output() submit$ = new EventEmitter(); 32 | 33 | constructor( 34 | private readonly threadService: ThreadService, 35 | private readonly configurationFormService: ConfigurationFormService, 36 | ) {} 37 | 38 | startChat(): void { 39 | this.threadService 40 | .start() 41 | .pipe(take(1)) 42 | .subscribe(() => { 43 | this.submit$.emit(this.form.value as ConfigurationFormValue); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/shared/configuration-form.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FormControl, FormGroup } from '@angular/forms'; 3 | import { CreateThreadDto } from '@boldare/openai-assistant'; 4 | import { ConfigurationForm } from './configuration.model'; 5 | import { ChatMessageStatus, SpeechVoice } from '../../+chat/shared/chat.model'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class ConfigurationFormService { 9 | form = new FormGroup({ 10 | firstName: new FormControl(null), 11 | voice: new FormControl(SpeechVoice.Alloy, { nonNullable: true }), 12 | }); 13 | 14 | getInitialThreadMessages(): CreateThreadDto { 15 | return { 16 | messages: [ 17 | { 18 | role: 'user', 19 | content: `Below you can find my details: 20 | * first name: ${this.form.controls.firstName.value || '-'} 21 | `, 22 | metadata: { 23 | status: ChatMessageStatus.Invisible, 24 | }, 25 | }, 26 | ], 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/shared/configuration.helpers.ts: -------------------------------------------------------------------------------- 1 | import { SpeechVoice } from '../../+chat/shared/chat.model'; 2 | 3 | export const voices: SpeechVoice[] = [ 4 | SpeechVoice.Alloy, 5 | SpeechVoice.Echo, 6 | SpeechVoice.Fable, 7 | SpeechVoice.Nova, 8 | SpeechVoice.Onyx, 9 | SpeechVoice.Shimmer, 10 | ]; 11 | -------------------------------------------------------------------------------- /apps/spa/src/app/modules/+configuration/shared/configuration.model.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms'; 2 | import { SpeechVoice } from '../../+chat/shared/chat.model'; 3 | 4 | export interface ConfigurationForm { 5 | firstName: FormControl; 6 | voice: FormControl; 7 | } 8 | 9 | export interface ConfigurationFormValue { 10 | firstName: string | null; 11 | voice: SpeechVoice; 12 | } 13 | -------------------------------------------------------------------------------- /apps/spa/src/app/pipes/message-file.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { isImageFileContentBlock } from '../components/controls/message-content/message-content.helpers'; 3 | import { ChatMessage } from '../modules/+chat/shared/chat.model'; 4 | import { ImageFileContentBlock } from 'openai/resources/beta/threads'; 5 | 6 | @Pipe({ 7 | standalone: true, 8 | name: 'messageImageFile', 9 | pure: true, 10 | }) 11 | export class MessageImageFilePipe implements PipeTransform { 12 | transform(message: Partial): ImageFileContentBlock[] { 13 | if (typeof message.content === 'string') { 14 | return []; 15 | } 16 | 17 | return message?.content?.filter(isImageFileContentBlock) || []; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/spa/src/app/pipes/message-text.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { isTextContentBlock } from '../components/controls/message-content/message-content.helpers'; 3 | import { ChatMessage } from '../modules/+chat/shared/chat.model'; 4 | 5 | @Pipe({ 6 | standalone: true, 7 | name: 'messageText', 8 | pure: false, 9 | }) 10 | export class MessageTextPipe implements PipeTransform { 11 | transform(message: Partial): string { 12 | if (typeof message.content === 'string') { 13 | return message.content; 14 | } 15 | 16 | // @TODO: handle all types of message content 17 | return ( 18 | message.content 19 | ?.filter(isTextContentBlock) 20 | ?.map(block => block.text.value) 21 | ?.join(' ') || '' 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/spa/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/spa/src/assets/ai-assistant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/ai-assistant.jpg -------------------------------------------------------------------------------- /apps/spa/src/assets/ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/ai.jpg -------------------------------------------------------------------------------- /apps/spa/src/assets/ai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/spa/src/assets/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/avatar.jpeg -------------------------------------------------------------------------------- /apps/spa/src/assets/boldare-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/boldare-circle.png -------------------------------------------------------------------------------- /apps/spa/src/assets/boldare.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/spa/src/assets/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/js/.gitkeep -------------------------------------------------------------------------------- /apps/spa/src/assets/trigger.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/trigger.jpeg -------------------------------------------------------------------------------- /apps/spa/src/assets/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/apps/spa/src/assets/uploads/.gitkeep -------------------------------------------------------------------------------- /apps/spa/src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | const { protocol, hostname, port } = window.location; 2 | 3 | export const environment = { 4 | env: 'dev', 5 | appUrl: `${protocol}//${hostname}:${port}`, 6 | apiUrl: `${protocol}//${hostname}:3000/api`, 7 | websocketUrl: `${protocol}//${hostname}:3000`, 8 | isThreadMemorized: true, 9 | isAudioEnabled: true, 10 | isTranscriptionEnabled: true, 11 | isAttachmentEnabled: true, 12 | isRefreshEnabled: true, 13 | isConfigEnabled: false, 14 | isAutoOpen: true, 15 | isStreamingEnabled: true, 16 | isImageContentEnabled: true, 17 | }; 18 | -------------------------------------------------------------------------------- /apps/spa/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | const { protocol, hostname, port } = window.location; 2 | 3 | export const environment = { 4 | env: 'prod', 5 | appUrl: `${protocol}//${hostname}:${port}`, 6 | apiUrl: `${protocol}//${hostname}:${port}/api`, 7 | websocketUrl: `${protocol}//${hostname}:${port}`, 8 | isThreadMemorized: true, 9 | isAudioEnabled: true, 10 | isTranscriptionEnabled: true, 11 | isAttachmentEnabled: true, 12 | isRefreshEnabled: true, 13 | isConfigEnabled: false, 14 | isAutoOpen: true, 15 | isStreamingEnabled: true, 16 | isImageContentEnabled: true, 17 | }; 18 | -------------------------------------------------------------------------------- /apps/spa/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boldare - AI Assistant 6 | 7 | 10 | 13 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/spa/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => 6 | console.error(err) 7 | ); 8 | -------------------------------------------------------------------------------- /apps/spa/src/styles/_extends/_markdown.scss: -------------------------------------------------------------------------------- 1 | .annotation { 2 | display: inline-block; 3 | text-align: center; 4 | padding: 2px 4px; 5 | font-size: 11px; 6 | font-weight: 400; 7 | min-width: 14px; 8 | border-radius: 4px; 9 | text-decoration: none; 10 | background-color: rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | .annotation__metadata { 14 | display: none; 15 | } 16 | -------------------------------------------------------------------------------- /apps/spa/src/styles/_settings/_animations.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --animation-fast: 0.2s all ease-in-out; 3 | } 4 | -------------------------------------------------------------------------------- /apps/spa/src/styles/_settings/_borders.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --border-radius-default: 8px; 3 | --border-radius-medium: 16px; 4 | } 5 | -------------------------------------------------------------------------------- /apps/spa/src/styles/_settings/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // Breakpoints 2 | // ------------------------------------------ 3 | // * device 4 | // * device-small 5 | // * device-medium 6 | // * device-big 7 | // ------------------------------------------ 8 | $mobile: 320px; 9 | $mobile-big: 560px; 10 | $tablet: 768px; 11 | $tablet-big: 1024px; 12 | $desktop: 1280px; 13 | $desktop-big: 1440px; 14 | 15 | // Breakpoints report 16 | // ------------------------------------------ 17 | $breakpoints: ( 18 | 'mobile': $mobile, 19 | 'tablet': $tablet, 20 | 'desktop': $desktop, 21 | 'desktop-big': $desktop-big, 22 | ); 23 | 24 | // Breakpoints mixins 25 | // ------------------------------------------ 26 | @mixin min-screen($resolution) { 27 | @media screen and (min-width: $resolution) { 28 | @content; 29 | } 30 | } 31 | 32 | @mixin max-screen($resolution) { 33 | @media screen and (max-width: $resolution - 1) { 34 | @content; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/spa/src/styles/_settings/_colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-transparent: rgba(0, 0, 0, 0); 3 | --color-white: #ffffff; 4 | --color-black: #000000; 5 | 6 | --color-grey-900: #2a2a2a; 7 | --color-grey-600: #464646; 8 | --color-grey-500: #656565; 9 | --color-grey-400: #a1a09f; 10 | --color-grey-300: #e0e0e0; 11 | --color-grey-200: #f2f2f2; 12 | 13 | --color-blue-500: #546874; 14 | 15 | --color-primary-200: #fae4cb; 16 | --color-primary-500: #a8875c; 17 | 18 | --color-red-500: #e83333; 19 | 20 | --color-green-500: #2ad82e; 21 | } 22 | -------------------------------------------------------------------------------- /apps/spa/src/styles/_settings/_shadow.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --shadow-light: rgba(6, 6, 12, 0.05) 0 2px 14px, 3 | rgba(6, 6, 12, 0.05) 0 4px 32px; 4 | --shadow-default: rgba(17, 17, 26, 0.1) 0 4px 16px, 5 | rgba(17, 17, 26, 0.1) 0 8px 32px; 6 | --shadow-default-hover: rgb(17, 17, 26, 0.3) 0 4px 26px, 7 | rgb(17, 17, 26, 0.1) 0 8px 32px; 8 | } 9 | -------------------------------------------------------------------------------- /apps/spa/src/styles/settings.scss: -------------------------------------------------------------------------------- 1 | @import '_settings/breakpoints'; 2 | @import '_settings/sizes'; 3 | -------------------------------------------------------------------------------- /apps/spa/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | @import '_settings/colors'; 4 | @import '_settings/animations'; 5 | @import '_settings/shadow'; 6 | @import '_settings/borders'; 7 | 8 | @import '_extends/material'; 9 | @import '_extends/markdown'; 10 | 11 | html, 12 | body { 13 | min-height: 100dvh; 14 | } 15 | 16 | body { 17 | min-height: 100dvh; 18 | margin: 0; 19 | background-color: #f5f7fa; 20 | background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); 21 | color: var(--color-grey-900); 22 | font-size: 12px; 23 | font-family: Roboto, 'Helvetica Neue', sans-serif; 24 | } 25 | -------------------------------------------------------------------------------- /apps/spa/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error: Jest global setup 2 | globalThis.ngJest = { 3 | testEnvironmentOptions: { 4 | errorOnUnknownElements: true, 5 | errorOnUnknownProperties: true, 6 | }, 7 | }; 8 | 9 | global.setImmediate = jest.useRealTimers as unknown as typeof setImmediate; 10 | // @ts-expect-error: Jest global setup 11 | window.setImmediate = window.setTimeout; 12 | import 'jest-preset-angular/setup-jest'; 13 | -------------------------------------------------------------------------------- /apps/spa/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts"], 8 | "include": ["src/**/*.d.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/spa/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/spa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "esModuleInterop": true 12 | }, 13 | "files": [], 14 | "include": [], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.app.json" 18 | }, 19 | { 20 | "path": "./tsconfig.spec.json" 21 | }, 22 | { 23 | "path": "./tsconfig.editor.json" 24 | } 25 | ], 26 | "extends": "../../tsconfig.base.json", 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/spa/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjectsAsync } from '@nx/jest'; 2 | 3 | export default async () => ({ 4 | projects: await getJestProjectsAsync(), 5 | }); 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/ai-embedded/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/ai-embedded/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript" 5 | }, 6 | "target": "es2016" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/ai-embedded/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ai-embedded', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | transform: { 7 | '^.+\\.[tj]s$': '@swc/jest', 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../../coverage/libs/ai-embedded', 11 | }; 12 | -------------------------------------------------------------------------------- /libs/ai-embedded/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-embedded", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/ai-embedded/src", 6 | "tags": [], 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/webpack:webpack", 10 | "outputs": ["{options.outputPath}"], 11 | "defaultConfiguration": "production", 12 | "options": { 13 | "target": "web", 14 | "outputPath": "dist/libs/ai-embedded", 15 | "compiler": "swc", 16 | "main": "libs/ai-embedded/src/index.ts", 17 | "tsConfig": "libs/ai-embedded/tsconfig.app.json", 18 | "webpackConfig": "libs/ai-embedded/webpack.config.js", 19 | "scripts": [] 20 | }, 21 | "configurations": { 22 | "production": { 23 | "optimization": true, 24 | "outputHashing": "none", 25 | "sourceMap": false, 26 | "namedChunks": false, 27 | "extractLicenses": false, 28 | "vendorChunk": false, 29 | "fileReplacements": [ 30 | { 31 | "replace": "libs/ai-embedded/src/environments/environment.ts", 32 | "with": "libs/ai-embedded/src/environments/environment.prod.ts" 33 | } 34 | ] 35 | } 36 | } 37 | }, 38 | "lint": { 39 | "executor": "@nx/eslint:lint", 40 | "outputs": ["{options.outputFile}"] 41 | }, 42 | "test": { 43 | "executor": "@nx/jest:jest", 44 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 45 | "options": { 46 | "jestConfig": "libs/ai-embedded/jest.config.ts" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /libs/ai-embedded/set-env.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs'; 2 | import { config } from 'dotenv'; 3 | import path from 'node:path'; 4 | 5 | config(); 6 | 7 | const environmentProdFilePath = path.resolve('./libs/ai-embedded/src/environments/environment.prod.ts'); 8 | 9 | const variableMap = { 10 | appUrl: 'APP_URL', 11 | }; 12 | 13 | const updateEnvironmentFile = (filePath) => { 14 | let content = readFileSync(filePath, 'utf8'); 15 | 16 | Object.keys(variableMap).forEach(key => { 17 | const envKey = variableMap[key]; 18 | const value = process.env[envKey]; 19 | if (value) { 20 | const regex = new RegExp(`(${key}:\\s*).*(,)`, 'g'); 21 | content = content.replace(regex, `$1'${value}'$2`); 22 | } 23 | }); 24 | 25 | writeFileSync(filePath, content, 'utf8'); 26 | console.log(`Updated ${filePath}`); 27 | }; 28 | 29 | updateEnvironmentFile(environmentProdFilePath); 30 | -------------------------------------------------------------------------------- /libs/ai-embedded/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | env: 'prod', 3 | appUrl: '', 4 | }; 5 | -------------------------------------------------------------------------------- /libs/ai-embedded/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | const { protocol, hostname, port } = window.location; 2 | 3 | export const environment = { 4 | env: 'dev', 5 | appUrl: `${protocol}//${hostname}:${port}`, 6 | }; 7 | -------------------------------------------------------------------------------- /libs/ai-embedded/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AssistantIframe } from './lib/assistant-iframe'; 2 | 3 | export * from './lib/assistant-iframe'; 4 | export * from './lib/assistant-iframe.model'; 5 | 6 | (function () { 7 | new AssistantIframe(); 8 | })(); 9 | -------------------------------------------------------------------------------- /libs/ai-embedded/src/lib/assistant-iframe.model.ts: -------------------------------------------------------------------------------- 1 | export interface AssistantIframeConfig { 2 | url: string; 3 | elementId: string; 4 | iframeId: string; 5 | iframeClass: string; 6 | toggleClass: string; 7 | toggleIsAnimated: boolean; 8 | bodyOpenClass: string; 9 | } 10 | -------------------------------------------------------------------------------- /libs/ai-embedded/src/test-setup.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/openai-assistant/aacad6b9dae17f44451daf788f0831a6151fdfcf/libs/ai-embedded/src/test-setup.ts -------------------------------------------------------------------------------- /libs/ai-embedded/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 8 | "include": ["src/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/ai-embedded/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/ai-embedded/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/ai-embedded/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { composePlugins, withNx } = require('@nx/webpack'); 2 | 3 | // Nx plugins for webpack. 4 | module.exports = composePlugins(withNx(), config => { 5 | // Update the webpack config as needed here. 6 | // e.g. `config.plugins.push(new MyPlugin())` 7 | return config; 8 | }); 9 | -------------------------------------------------------------------------------- /libs/openai-assistant/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.json"], 19 | "parser": "jsonc-eslint-parser", 20 | "rules": { 21 | "@nx/dependency-checks": 1 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /libs/openai-assistant/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | displayName: 'openai-assistant', 3 | preset: '../../jest.preset.js', 4 | testEnvironment: 'node', 5 | transform: { 6 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 7 | }, 8 | moduleFileExtensions: ['ts', 'js', 'html'], 9 | coverageDirectory: '../../coverage/libs/openai-assistant', 10 | }; 11 | -------------------------------------------------------------------------------- /libs/openai-assistant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boldare/openai-assistant", 3 | "description": "NestJS library for building chatbot solutions based on the OpenAI Assistant API", 4 | "version": "1.2.1", 5 | "private": false, 6 | "dependencies": { 7 | "tslib": "^2.6.3", 8 | "dotenv": "^16.4.5", 9 | "envfile": "^7.1.0", 10 | "@nestjs/common": "^10.4.4", 11 | "@nestjs/platform-express": "^10.4.4", 12 | "@nestjs/axios": "^3.0.3", 13 | "@nestjs/swagger": "^7.4.2", 14 | "@nestjs/websockets": "^10.3.0", 15 | "multer": "^1.4.4-lts.1", 16 | "class-validator": "^0.14.1", 17 | "socket.io": "^4.8.0" 18 | }, 19 | "peerDependencies": { 20 | "openai": "^4.67.3" 21 | }, 22 | "type": "commonjs", 23 | "main": "./src/index.js", 24 | "typings": "./src/index.d.ts", 25 | "homepage": "https://github.com/boldare/openai-assistant", 26 | "keywords": [ 27 | "ai", 28 | "assistant", 29 | "boldare", 30 | "nestjs", 31 | "openai", 32 | "chatbot", 33 | "bot", 34 | "assistant", 35 | "ai-assistant", 36 | "openai-assistant" 37 | ], 38 | "author": "boldare", 39 | "license": "MIT", 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/boldare/openai-assistant.git" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/openai-assistant/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-assistant", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/openai-assistant/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/openai-assistant", 12 | "tsConfig": "libs/openai-assistant/tsconfig.lib.json", 13 | "packageJson": "libs/openai-assistant/package.json", 14 | "main": "libs/openai-assistant/src/index.ts", 15 | "assets": ["libs/openai-assistant/*.md"] 16 | } 17 | }, 18 | "publish": { 19 | "command": "node tools/scripts/publish.mjs assistant-ai {args.ver} {args.tag}", 20 | "dependsOn": ["build"] 21 | }, 22 | "lint": { 23 | "executor": "@nx/eslint:lint", 24 | "outputs": ["{options.outputFile}"] 25 | }, 26 | "test": { 27 | "executor": "@nx/jest:jest", 28 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 29 | "options": { 30 | "jestConfig": "libs/openai-assistant/jest.config.ts" 31 | } 32 | } 33 | }, 34 | "tags": [] 35 | } 36 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/assistant'; 2 | export * from './lib/agent'; 3 | export * from './lib/ai'; 4 | export * from './lib/chat'; 5 | export * from './lib/run'; 6 | export * from './lib/files'; 7 | export * from './lib/threads'; 8 | export * from './lib/annotations'; 9 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.base.spec.ts: -------------------------------------------------------------------------------- 1 | import { definitionMock } from './agent.mock'; 2 | import { AgentService } from './agent.service'; 3 | import { AgentBase } from './agent.base'; 4 | import { AgentData } from './agent.model'; 5 | 6 | describe('AgentBase', () => { 7 | let agentBase: AgentBase; 8 | let agentService: AgentService; 9 | 10 | beforeEach(() => { 11 | agentService = new AgentService(); 12 | agentBase = new AgentBase(agentService); 13 | }); 14 | 15 | describe('onModuleInit', () => { 16 | it('should call agentService.add with definition and output', () => { 17 | const addSpy = jest.spyOn(agentService, 'add'); 18 | agentBase.definition = definitionMock; 19 | 20 | agentBase.onModuleInit(); 21 | 22 | expect(addSpy).toHaveBeenCalledWith(definitionMock, expect.any(Function)); 23 | }); 24 | }); 25 | 26 | describe('output', () => { 27 | it('should return empty string when params are missing', async () => { 28 | const result = await agentBase.output({} as AgentData); 29 | 30 | expect(result).toBe(''); 31 | }); 32 | 33 | it('should return params when they are provided', async () => { 34 | const result = await agentBase.output({ params: 'test' } as AgentData); 35 | 36 | expect(result).toBe('test'); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.base.ts: -------------------------------------------------------------------------------- 1 | import { OnModuleInit } from '@nestjs/common'; 2 | import { FunctionTool } from 'openai/resources/beta'; 3 | import { AgentService } from './agent.service'; 4 | import { AgentData } from './agent.model'; 5 | 6 | export class AgentBase implements OnModuleInit { 7 | definition!: FunctionTool; 8 | 9 | onModuleInit(): void { 10 | this.agentService.add(this.definition, this.output.bind(this)); 11 | } 12 | 13 | constructor(protected readonly agentService: AgentService) {} 14 | 15 | async output(data: AgentData): Promise { 16 | return data.params || ''; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.mock.ts: -------------------------------------------------------------------------------- 1 | import { FunctionTool } from 'openai/resources/beta'; 2 | 3 | export const agentNameMock = 'agent-name'; 4 | 5 | export const agentMock = async () => 'agent-result'; 6 | 7 | export const definitionMock: FunctionTool = { 8 | type: 'function', 9 | function: { name: agentNameMock }, 10 | }; 11 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.model.ts: -------------------------------------------------------------------------------- 1 | export type Agent = (data: AgentData) => Promise; 2 | export type Agents = Record; 3 | 4 | export interface AgentData { 5 | threadId: string; 6 | params: string; 7 | } 8 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AgentService } from './agent.service'; 3 | 4 | @Module({ 5 | providers: [AgentService], 6 | exports: [AgentService], 7 | }) 8 | export class AgentModule {} 9 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { AgentService } from './agent.service'; 2 | import { agentMock, agentNameMock, definitionMock } from './agent.mock'; 3 | 4 | describe('AgentService', () => { 5 | let agentService: AgentService; 6 | 7 | beforeEach(() => { 8 | agentService = new AgentService(); 9 | }); 10 | 11 | it('should be defined', () => { 12 | expect(agentService).toBeDefined(); 13 | }); 14 | 15 | it('agentMock should be return value', async () => { 16 | const result = await agentMock(); 17 | 18 | expect(result).toEqual('agent-result'); 19 | }); 20 | 21 | describe('add', () => { 22 | it('should add new tool', async () => { 23 | agentService.add(definitionMock, agentMock); 24 | expect(agentService.tools).toContain(definitionMock); 25 | }); 26 | 27 | it('should add new agent', async () => { 28 | agentService.add(definitionMock, agentMock); 29 | expect(agentService.agents[agentNameMock]).toEqual(agentMock); 30 | }); 31 | }); 32 | 33 | describe('get', () => { 34 | it('should return agent', async () => { 35 | agentService.add(definitionMock, agentMock); 36 | expect(agentService.get(agentNameMock)).toEqual(agentMock); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/agent.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Agent, Agents } from './agent.model'; 3 | import { FunctionTool } from 'openai/resources/beta'; 4 | 5 | @Injectable() 6 | export class AgentService { 7 | public agents: Agents = {}; 8 | public tools: FunctionTool[] = []; 9 | 10 | add(definition: FunctionTool, fn: Agent): void { 11 | this.tools.push(definition); 12 | this.agents[definition.function.name] = fn; 13 | } 14 | 15 | get(name: string): Agent { 16 | return this.agents[name]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/agent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent.base'; 2 | export * from './agent.model'; 3 | export * from './agent.service'; 4 | export * from './agent.module'; 5 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/ai/ai.mock.ts: -------------------------------------------------------------------------------- 1 | import { AiTranscription } from './ai.model'; 2 | // @ts-expect-error multer is necessary 3 | // eslint-disable-next-line 4 | import { multer } from 'multer'; 5 | 6 | export const mockBuffer = Buffer.from('fake audio data'); 7 | 8 | export const mockFileData = { 9 | buffer: mockBuffer, 10 | mimetype: 'audio/wav', 11 | } as Express.Multer.File; 12 | 13 | export const transcriptionMock: AiTranscription = { 14 | text: 'Text from transcription', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/ai/ai.model.ts: -------------------------------------------------------------------------------- 1 | import { Transcription } from 'openai/resources/audio'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export enum SpeechVoice { 5 | Alloy = 'alloy', 6 | Echo = 'echo', 7 | Fable = 'fable', 8 | Onyx = 'onyx', 9 | Nova = 'nova', 10 | Shimmer = 'shimmer', 11 | } 12 | 13 | export class PostSpeechDto { 14 | @ApiProperty({ description: 'Content of the message' }) 15 | content!: string; 16 | 17 | @ApiProperty({ 18 | description: 'Voice of the message author.', 19 | enum: SpeechVoice, 20 | required: false, 21 | }) 22 | voice?: SpeechVoice; 23 | } 24 | 25 | export class PostSpeechResponseDto { 26 | @ApiProperty() 27 | type!: 'buffer'; 28 | 29 | @ApiProperty({ type: 'number', isArray: true }) 30 | data!: number[]; 31 | } 32 | 33 | export class PostTranscriptionDto { 34 | @ApiProperty({ type: 'string', format: 'binary' }) 35 | file!: File; 36 | } 37 | 38 | export class PostTranscriptionResponseDto { 39 | @ApiProperty({ description: 'Transcription of the audio file' }) 40 | content!: string; 41 | } 42 | 43 | export type AiTranscription = Transcription; 44 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/ai/ai.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AiService } from './ai.service'; 3 | import { AiController } from './ai.controller'; 4 | 5 | @Module({ 6 | providers: [AiService], 7 | controllers: [AiController], 8 | exports: [AiService], 9 | }) 10 | export class AiModule {} 11 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/ai/ai.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { toFile } from 'openai/uploads'; 2 | import { Response } from 'openai/core'; 3 | import { AiService } from './ai.service'; 4 | import { mockBuffer, transcriptionMock } from './ai.mock'; 5 | 6 | describe('AiService', () => { 7 | let aiService: AiService; 8 | 9 | beforeEach(() => { 10 | aiService = new AiService(); 11 | 12 | jest 13 | .spyOn(aiService.provider.audio.transcriptions, 'create') 14 | .mockResolvedValue(transcriptionMock); 15 | 16 | jest.spyOn(aiService.provider.audio.speech, 'create').mockResolvedValue({ 17 | arrayBuffer: jest.fn().mockResolvedValue(mockBuffer), 18 | } as unknown as Response); 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it('should return transcription', async () => { 26 | const file = await toFile(mockBuffer, 'audio.mp3', { type: 'mp3' }); 27 | 28 | const transcription = await aiService.transcription(file); 29 | 30 | expect(transcription.text).toBe(transcriptionMock.text); 31 | }); 32 | 33 | it('should return speech', async () => { 34 | const data = { content: 'test content' }; 35 | 36 | const speech = await aiService.speech(data); 37 | 38 | expect(speech).toStrictEqual(mockBuffer); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/ai/ai.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import OpenAI from 'openai'; 3 | import { Uploadable } from 'openai/uploads'; 4 | import { AiTranscription, PostSpeechDto, SpeechVoice } from './ai.model'; 5 | import 'dotenv/config'; 6 | 7 | @Injectable() 8 | export class AiService { 9 | provider = new OpenAI({ 10 | apiKey: process.env['OPENAI_API_KEY'] || '', 11 | }); 12 | 13 | async transcription(file: Uploadable): Promise { 14 | return this.provider.audio.transcriptions.create({ 15 | file, 16 | model: 'whisper-1', 17 | }); 18 | } 19 | 20 | async speech(data: PostSpeechDto): Promise { 21 | const response = await this.provider.audio.speech.create({ 22 | model: 'tts-1', 23 | voice: data.voice || SpeechVoice.Alloy, 24 | input: data.content, 25 | }); 26 | 27 | return Buffer.from(await response.arrayBuffer()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/ai/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ai.service'; 2 | export * from './ai.module'; 3 | export * from './ai.model'; 4 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/annotations/annotations.model.ts: -------------------------------------------------------------------------------- 1 | import { FileObject } from 'openai/resources'; 2 | import { Annotation } from 'openai/resources/beta/threads/messages'; 3 | 4 | export interface AnnotationData { 5 | annotation: Annotation; 6 | index: number; 7 | file: FileObject; 8 | } 9 | 10 | export enum AnnotationType { 11 | file_citation = 'file_citation', 12 | file_path = 'file_path', 13 | } 14 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/annotations/annotations.utils.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { 3 | FileCitationAnnotation, 4 | FilePathAnnotation, 5 | Message, 6 | } from 'openai/resources/beta/threads'; 7 | import { AnnotationData, AnnotationType } from './annotations.model'; 8 | 9 | export const isFileCitation = (item: { 10 | type: string; 11 | }): item is FileCitationAnnotation => item.type === 'file_citation'; 12 | 13 | export const isFilePath = (item: { 14 | type: string; 15 | }): item is FilePathAnnotation => item.type === 'file_path'; 16 | 17 | export const getAnnotations = async ( 18 | message: Message, 19 | provider: OpenAI, 20 | ): Promise => { 21 | if (message.content[0].type !== 'text') { 22 | return []; 23 | } 24 | 25 | const { text } = message.content[0]; 26 | const { annotations } = text; 27 | const annotationsData: AnnotationData[] = []; 28 | 29 | let index = 1; 30 | 31 | for (const annotation of annotations) { 32 | let data = null; 33 | 34 | if (isFileCitation(annotation)) { 35 | data = annotation[AnnotationType.file_citation]; 36 | } 37 | 38 | if (isFilePath(annotation)) { 39 | data = annotation[AnnotationType.file_path]; 40 | } 41 | 42 | if (data) { 43 | const file = await provider.files.retrieve(data.file_id); 44 | annotationsData.push({ annotation, index, file }); 45 | } 46 | 47 | index++; 48 | } 49 | 50 | return annotationsData; 51 | }; 52 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/annotations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './annotations.model'; 2 | export * from './annotations.utils'; 3 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/assistant/assistant-memory.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { writeFile, readFile } from 'fs/promises'; 3 | import * as envfile from 'envfile'; 4 | import * as dotenv from 'dotenv'; 5 | 6 | @Injectable() 7 | export class AssistantMemoryService { 8 | private readonly logger = new Logger(AssistantMemoryService.name); 9 | 10 | async saveAssistantId(id: string): Promise { 11 | try { 12 | const sourcePath = './.env'; 13 | const envVariables = await readFile(sourcePath); 14 | const parsedVariables = envfile.parse(envVariables.toString()); 15 | const newVariables = { 16 | ...parsedVariables, 17 | ASSISTANT_ID: id, 18 | }; 19 | 20 | process.env['ASSISTANT_ID'] = id; 21 | 22 | await writeFile(sourcePath, envfile.stringify(newVariables)); 23 | dotenv.config({ path: sourcePath }); 24 | } catch (error) { 25 | this.logger.error(`Can't save variable: ${error}`); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/assistant/assistant.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { Assistant } from 'openai/resources/beta'; 3 | import { AssistantService } from './assistant.service'; 4 | import { AssistantUpdate } from './assistant.model'; 5 | 6 | @Controller('assistant') 7 | export class AssistantController { 8 | constructor(public readonly assistantService: AssistantService) {} 9 | 10 | @Post('') 11 | async updateAssistant( 12 | @Body() { toolResources }: AssistantUpdate, 13 | ): Promise { 14 | return this.assistantService.updateFiles(toolResources); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/assistant/assistant.mock.ts: -------------------------------------------------------------------------------- 1 | import { AssistantCreateParams } from 'openai/resources/beta'; 2 | import { AssistantConfigParams } from './assistant.model'; 3 | 4 | export const assistantParamsMock: AssistantCreateParams = { 5 | name: '@boldare/tests', 6 | instructions: `test instructions`, 7 | tools: [{ type: 'file_search' }], 8 | model: 'gpt-3.5-turbo', 9 | metadata: {}, 10 | }; 11 | 12 | export const assistantConfigMock: AssistantConfigParams = { 13 | id: 'test1234', 14 | params: assistantParamsMock, 15 | filesDir: './apps/api/src/app/knowledge', 16 | toolResources: null, 17 | }; 18 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/assistant/assistant.model.ts: -------------------------------------------------------------------------------- 1 | import { AssistantCreateParams } from 'openai/resources/beta'; 2 | import { RequestOptions } from 'openai/core'; 3 | 4 | export interface AssistantConfigParams { 5 | id: string; 6 | params: AssistantCreateParams; 7 | options?: RequestOptions; 8 | filesDir?: string; 9 | toolResources?: AssistantToolResources | null; 10 | } 11 | 12 | export interface AssistantToolResources { 13 | fileSearch?: AssistantFileSearch; 14 | codeInterpreter?: AssistantCodeInterpreter; 15 | } 16 | 17 | export interface AssistantUpdate { 18 | toolResources: AssistantToolResources; 19 | } 20 | 21 | export type AssistantFileSearch = Record | null; 22 | export type AssistantCodeInterpreter = Record<'fileNames', string[]> | null; 23 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/assistant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assistant.service'; 2 | export * from './assistant.model'; 3 | export * from './assistant-files.service'; 4 | export * from './assistant-memory.service'; 5 | export * from './assistant.module'; 6 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/chat/chat.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { ChatController } from './chat.controller'; 3 | import { AiModule } from './../ai/ai.module'; 4 | import { ChatModule } from './chat.module'; 5 | import { ChatService } from './chat.service'; 6 | import { ChatCallDto, ChatCallResponseDto } from './chat.model'; 7 | 8 | describe('ChatController', () => { 9 | let chatController: ChatController; 10 | let chatService: ChatService; 11 | 12 | beforeEach(async () => { 13 | const moduleRef = await Test.createTestingModule({ 14 | imports: [AiModule, ChatModule], 15 | controllers: [ChatController], 16 | }).compile(); 17 | 18 | chatController = moduleRef.get(ChatController); 19 | chatService = moduleRef.get(ChatService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(chatController).toBeDefined(); 24 | }); 25 | 26 | describe('call', () => { 27 | it('should call chatService.call', async () => { 28 | jest 29 | .spyOn(chatService, 'call') 30 | .mockResolvedValue({} as ChatCallResponseDto); 31 | const payload = { 32 | threadId: '123', 33 | content: [ 34 | { 35 | text: { value: 'Hello', annotations: [] }, 36 | type: 'text', 37 | }, 38 | ], 39 | } as ChatCallDto; 40 | 41 | await chatController.call(payload); 42 | 43 | expect(chatService.call).toHaveBeenCalledWith(payload); 44 | }); 45 | }); 46 | 47 | afterEach(() => { 48 | jest.clearAllMocks(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/chat/chat.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { ChatCallDto, ChatCallResponseDto } from './chat.model'; 4 | import { ChatService } from './chat.service'; 5 | 6 | @ApiTags('Chat') 7 | @Controller('assistant/chat') 8 | export class ChatController { 9 | constructor(public readonly chatsService: ChatService) {} 10 | 11 | @ApiResponse({ 12 | status: 200, 13 | type: ChatCallResponseDto, 14 | description: 'Default action for conversation between user and bot', 15 | }) 16 | @ApiBody({ type: ChatCallDto }) 17 | @Post() 18 | async call(@Body() payload: ChatCallDto): Promise { 19 | return await this.chatsService.call(payload); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/chat/chat.gateway.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { Socket } from 'socket.io'; 3 | import { ChatGateway } from './chat.gateway'; 4 | import { ChatModule } from './chat.module'; 5 | import { ChatService } from './chat.service'; 6 | import { ChatCallDto } from './chat.model'; 7 | 8 | describe('ChatGateway', () => { 9 | let chatGateway: ChatGateway; 10 | let chatService: ChatService; 11 | 12 | beforeEach(async () => { 13 | const moduleRef = await Test.createTestingModule({ 14 | imports: [ChatModule], 15 | providers: [ChatGateway], 16 | }).compile(); 17 | 18 | chatService = moduleRef.get(ChatService); 19 | chatGateway = new ChatGateway(chatService); 20 | 21 | jest.spyOn(chatService, 'call').mockResolvedValue({ 22 | threadId: '123', 23 | content: [ 24 | { 25 | text: { value: 'Hello', annotations: [] }, 26 | type: 'text', 27 | }, 28 | ], 29 | }); 30 | }); 31 | 32 | it('should be defined', () => { 33 | expect(chatGateway).toBeDefined(); 34 | }); 35 | 36 | describe('listenForMessages', () => { 37 | it('should call chatService.call', async () => { 38 | const request = { 39 | threadId: '123', 40 | content: [ 41 | { 42 | text: { value: 'Hello', annotations: [] }, 43 | type: 'text', 44 | }, 45 | ], 46 | } as ChatCallDto; 47 | 48 | await chatGateway.listenForMessages(request, {} as Socket); 49 | 50 | expect(chatService.call).toHaveBeenCalled(); 51 | }); 52 | }); 53 | 54 | afterEach(() => { 55 | jest.clearAllMocks(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/chat/chat.helpers.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Message, MessageContent, Run } from 'openai/resources/beta/threads'; 3 | import { AiService } from '../ai'; 4 | 5 | @Injectable() 6 | export class ChatHelpers { 7 | private readonly provider = this.aiService.provider; 8 | private readonly threads = this.provider.beta.threads; 9 | 10 | constructor(private readonly aiService: AiService) {} 11 | 12 | async getAnswer(run: Run): Promise { 13 | const lastThreadMessage = await this.geRunMessage(run); 14 | return this.parseThreadMessage(lastThreadMessage); 15 | } 16 | 17 | parseThreadMessage(message?: Message): MessageContent[] { 18 | if (!message) { 19 | throw `Seems I'm lost, would you mind reformulating your question`; 20 | } 21 | 22 | return message.content; 23 | } 24 | 25 | async geRunMessage( 26 | run: Run, 27 | role = 'assistant', 28 | ): Promise { 29 | const messages = await this.threads.messages.list(run.thread_id); 30 | return ( 31 | messages.data 32 | .filter(message => message.run_id === run.id && message.role === role) 33 | .pop() || undefined 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AiModule } from '../ai'; 3 | import { RunModule } from '../run'; 4 | import { ChatHelpers } from './chat.helpers'; 5 | import { ChatService } from './chat.service'; 6 | import { SocketModule } from '@nestjs/websockets/socket-module'; 7 | import { ChatController } from './chat.controller'; 8 | 9 | export const sharedServices = [ChatHelpers, ChatService]; 10 | 11 | @Module({ 12 | imports: [SocketModule, AiModule, RunModule], 13 | providers: [...sharedServices], 14 | controllers: [ChatController], 15 | exports: [...sharedServices], 16 | }) 17 | export class ChatModule {} 18 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat.service'; 2 | export * from './chat.model'; 3 | export * from './chat.controller'; 4 | export * from './chat.helpers'; 5 | export * from './chat.module'; 6 | export * from './chat.gateway'; 7 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ConfigService], 6 | exports: [ConfigService], 7 | }) 8 | export class ConfigModule {} 9 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/config/config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from './config.service'; 2 | import { AssistantConfigParams } from '../assistant/assistant.model'; 3 | 4 | describe('ConfigService', () => { 5 | let configService: ConfigService; 6 | 7 | beforeEach(() => { 8 | configService = new ConfigService(); 9 | }); 10 | 11 | it('should be defined', () => { 12 | expect(configService).toBeDefined(); 13 | }); 14 | 15 | it('should set and get config', () => { 16 | const config = { id: '1' } as AssistantConfigParams; 17 | 18 | configService.set(config); 19 | 20 | expect(configService.get()).toEqual(config); 21 | expect(configService.get().id).toEqual('1'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AssistantConfigParams } from '../assistant'; 3 | 4 | @Injectable() 5 | export class ConfigService { 6 | params!: AssistantConfigParams; 7 | 8 | set(params: AssistantConfigParams): void { 9 | this.params = params; 10 | } 11 | 12 | get(): AssistantConfigParams { 13 | return this.params; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config.service'; 2 | export * from './config.module'; 3 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/files.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { FilesController } from './files.controller'; 3 | import { FilesModule } from './files.module'; 4 | import { FileObject } from 'openai/resources'; 5 | describe('FilesController', () => { 6 | let filesController: FilesController; 7 | 8 | beforeEach(async () => { 9 | const moduleRef = await Test.createTestingModule({ 10 | imports: [FilesModule], 11 | controllers: [FilesController], 12 | }).compile(); 13 | 14 | filesController = moduleRef.get(FilesController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(filesController).toBeDefined(); 19 | }); 20 | 21 | describe('updateFiles', () => { 22 | it('should call filesService.files', async () => { 23 | const spyOnFiles = jest 24 | .spyOn(filesController.filesService, 'files') 25 | .mockResolvedValue([{ id: '1' }] as FileObject[]); 26 | 27 | await filesController.updateFiles({ files: [] }); 28 | 29 | expect(spyOnFiles).toHaveBeenCalled(); 30 | }); 31 | }); 32 | 33 | afterEach(() => { 34 | jest.clearAllMocks(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/files.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Param, 5 | Post, 6 | UploadedFiles, 7 | UseInterceptors, 8 | } from '@nestjs/common'; 9 | import { FileFieldsInterceptor } from '@nestjs/platform-express'; 10 | import { FilesService } from './files.service'; 11 | import { UploadFilesDto, UploadFilesResponseDto } from './files.model'; 12 | import { ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger'; 13 | import { FileObject } from 'openai/resources'; 14 | 15 | @ApiTags('Files') 16 | @Controller('assistant/files') 17 | export class FilesController { 18 | constructor(public readonly filesService: FilesService) {} 19 | 20 | @ApiConsumes('multipart/form-data') 21 | @ApiResponse({ status: 200, type: UploadFilesResponseDto }) 22 | @ApiBody({ type: UploadFilesDto }) 23 | @Post('/') 24 | @UseInterceptors(FileFieldsInterceptor([{ name: 'files', maxCount: 10 }])) 25 | async updateFiles( 26 | @UploadedFiles() uploadedData: { files: Express.Multer.File[] }, 27 | ): Promise { 28 | return { 29 | files: await this.filesService.files(uploadedData.files), 30 | }; 31 | } 32 | 33 | @Get('/retrive/:fileId') 34 | async retriveFile(@Param() params: { fileId: string }): Promise { 35 | return this.filesService.retriveFile(params.fileId); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/files.model.ts: -------------------------------------------------------------------------------- 1 | import { FileObject } from 'openai/resources'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export type OpenAiFile = FileObject; 5 | 6 | export interface UploadFilesPayload { 7 | files: File[]; 8 | } 9 | 10 | export class UploadFile { 11 | @ApiProperty({ description: 'Unique identifier of the file' }) 12 | id!: string; 13 | 14 | @ApiProperty() 15 | bytes!: number; 16 | 17 | @ApiProperty({ description: 'Datetime the file was created.' }) 18 | created_at!: number; 19 | 20 | @ApiProperty({ description: 'Name of the file' }) 21 | filename!: string; 22 | 23 | @ApiProperty() 24 | object!: 'file'; 25 | 26 | @ApiProperty() 27 | purpose!: 28 | | 'fine-tune' 29 | | 'fine-tune-results' 30 | | 'assistants' 31 | | 'assistants_output'; 32 | 33 | @ApiProperty() 34 | status!: 'uploaded' | 'processed' | 'error'; 35 | 36 | @ApiProperty() 37 | status_details?: string; 38 | } 39 | 40 | export class UploadFilesResponseDto { 41 | @ApiProperty({ 42 | isArray: true, 43 | type: UploadFile, 44 | }) 45 | files!: FileObject[]; 46 | } 47 | 48 | export class UploadFilesDto { 49 | @ApiProperty({ type: 'string', isArray: true, format: 'binary' }) 50 | files!: File[]; 51 | } 52 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/files.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FilesService } from './files.service'; 3 | import { FilesController } from './files.controller'; 4 | import { AiModule } from '../ai'; 5 | 6 | @Module({ 7 | imports: [AiModule], 8 | providers: [FilesService], 9 | controllers: [FilesController], 10 | exports: [FilesService], 11 | }) 12 | export class FilesModule {} 13 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/files.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { FilesModule } from './files.module'; 3 | import { FilesService } from './files.service'; 4 | import { FileObject } from 'openai/resources'; 5 | 6 | describe('FilesService', () => { 7 | let filesService: FilesService; 8 | 9 | beforeEach(async () => { 10 | const moduleRef = await Test.createTestingModule({ 11 | imports: [FilesModule], 12 | }).compile(); 13 | 14 | filesService = moduleRef.get(FilesService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(filesService).toBeDefined(); 19 | }); 20 | 21 | describe('files', () => { 22 | it('should return an array of FileObject', async () => { 23 | const files = [{ buffer: Buffer.from('file') }]; 24 | jest 25 | .spyOn(filesService.provider.files, 'create') 26 | .mockResolvedValue({ id: '1' } as FileObject); 27 | 28 | const result = await filesService.files(files as Express.Multer.File[]); 29 | 30 | expect(result).toEqual([{ id: '1' }]); 31 | }); 32 | }); 33 | 34 | afterEach(() => { 35 | jest.clearAllMocks(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/files.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { toFile } from 'openai/uploads'; 3 | import { FileObject } from 'openai/resources'; 4 | import { AiService } from '../ai'; 5 | // @ts-expect-error multer is necessary 6 | // eslint-disable-next-line 7 | import { multer } from 'multer'; 8 | 9 | @Injectable() 10 | export class FilesService { 11 | provider = this.aiService.provider; 12 | 13 | constructor(private readonly aiService: AiService) {} 14 | 15 | async files(files: Express.Multer.File[]): Promise { 16 | return await Promise.all( 17 | files.map(async file => { 18 | return this.provider.files.create({ 19 | file: await toFile(file.buffer, file.originalname), 20 | purpose: 'assistants', 21 | }); 22 | }), 23 | ); 24 | } 25 | 26 | async retriveFile(fileId: string): Promise { 27 | return await this.provider.files.retrieve(fileId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from './files.model'; 2 | export * from './files.controller'; 3 | export * from './files.service'; 4 | export * from './files.module'; 5 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/run/index.ts: -------------------------------------------------------------------------------- 1 | export * from './run.service'; 2 | export * from './run.module'; 3 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/run/run.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AiModule } from '../ai'; 3 | import { AgentModule } from '../agent'; 4 | import { RunService } from './run.service'; 5 | 6 | @Module({ 7 | imports: [AiModule, AgentModule], 8 | providers: [RunService], 9 | exports: [RunService], 10 | }) 11 | export class RunModule {} 12 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/threads/index.ts: -------------------------------------------------------------------------------- 1 | export * from './threads.controller'; 2 | export * from './threads.service'; 3 | export * from './threads.module'; 4 | export * from './threads.model'; 5 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/threads/threads.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { ThreadsController } from './threads.controller'; 3 | import { Thread } from 'openai/resources/beta'; 4 | import { ThreadsModule } from './threads.module'; 5 | import { ThreadsService } from './threads.service'; 6 | import { GetThreadResponseDto } from './threads.model'; 7 | 8 | describe('ThreadsController', () => { 9 | let threadsController: ThreadsController; 10 | let threadsService: ThreadsService; 11 | 12 | beforeEach(async () => { 13 | const moduleRef = await Test.createTestingModule({ 14 | imports: [ThreadsModule], 15 | controllers: [ThreadsController], 16 | }).compile(); 17 | 18 | threadsController = moduleRef.get(ThreadsController); 19 | threadsService = moduleRef.get(ThreadsService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(threadsController).toBeDefined(); 24 | }); 25 | 26 | describe('getThread', () => { 27 | it('should call threadsService.getThread', async () => { 28 | const spyOnGetThread = jest 29 | .spyOn(threadsService, 'getThread') 30 | .mockResolvedValue({} as GetThreadResponseDto); 31 | 32 | await threadsController.getThread({ id: '1' }); 33 | 34 | expect(spyOnGetThread).toHaveBeenCalled(); 35 | }); 36 | }); 37 | 38 | describe('createThread', () => { 39 | it('should call threadsService.createThread', async () => { 40 | const spyOnCreateThread = jest 41 | .spyOn(threadsService, 'createThread') 42 | .mockResolvedValue({} as Thread); 43 | 44 | await threadsController.createThread({}); 45 | 46 | expect(spyOnCreateThread).toHaveBeenCalled(); 47 | }); 48 | }); 49 | 50 | afterEach(() => { 51 | jest.clearAllMocks(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/threads/threads.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post } from '@nestjs/common'; 2 | import { 3 | GetThreadDto, 4 | CreateThreadDto, 5 | CreateThreadResponseDto, 6 | GetThreadResponseDto, 7 | } from './threads.model'; 8 | import { ThreadsService } from './threads.service'; 9 | import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; 10 | 11 | @ApiTags('Threads') 12 | @Controller('assistant/threads') 13 | export class ThreadsController { 14 | constructor(private readonly threadsService: ThreadsService) {} 15 | 16 | @ApiResponse({ status: 200, type: GetThreadResponseDto }) 17 | @Get(':id') 18 | async getThread( 19 | @Param() params: GetThreadDto, 20 | ): Promise { 21 | return await this.threadsService.getThread(params.id); 22 | } 23 | 24 | @ApiResponse({ status: 200, type: CreateThreadResponseDto }) 25 | @ApiBody({ type: CreateThreadDto, required: false }) 26 | @Post('') 27 | async createThread( 28 | @Body() payload?: CreateThreadDto, 29 | ): Promise { 30 | return await this.threadsService.createThread(payload); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/threads/threads.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ThreadsController } from './threads.controller'; 3 | import { ThreadsService } from './threads.service'; 4 | import { AiModule } from '../ai'; 5 | 6 | @Module({ 7 | imports: [AiModule], 8 | providers: [ThreadsService], 9 | controllers: [ThreadsController], 10 | exports: [ThreadsService], 11 | }) 12 | export class ThreadsModule {} 13 | -------------------------------------------------------------------------------- /libs/openai-assistant/src/lib/threads/threads.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Thread } from 'openai/resources/beta'; 3 | import { AiService } from '../ai'; 4 | import { CreateThreadDto, GetThreadResponseDto } from './threads.model'; 5 | 6 | @Injectable() 7 | export class ThreadsService { 8 | constructor(private readonly aiService: AiService) {} 9 | 10 | async getThread(id: string): Promise { 11 | const messages = 12 | await this.aiService.provider.beta.threads.messages.list(id); 13 | return { 14 | id, 15 | messages: messages?.data || [], 16 | }; 17 | } 18 | 19 | async createThread({ messages }: CreateThreadDto = {}): Promise { 20 | return this.aiService.provider.beta.threads.create({ messages }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/openai-assistant/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /libs/openai-assistant/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"], 7 | "target": "es2021", 8 | "strictNullChecks": true, 9 | "strictBindCallApply": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /libs/openai-assistant/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boldare/source", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "local-registry": { 6 | "executor": "@nx/js:verdaccio", 7 | "options": { 8 | "port": 4873, 9 | "config": ".verdaccio/config.yml", 10 | "storage": "tmp/local-registry/storage" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@boldare/ai-embedded": ["libs/ai-embedded/src/index.ts"], 19 | "@boldare/openai-assistant": ["libs/openai-assistant/src/index.ts"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | --------------------------------------------------------------------------------