├── .editorconfig ├── .eslintignore ├── .eslintrc.base.json ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .verdaccio └── config.yml ├── .vscode └── extensions.json ├── README.md ├── client ├── .eslintrc.json ├── jest.config.ts ├── project.json ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── chat │ │ │ ├── chat.component.html │ │ │ ├── chat.component.scss │ │ │ └── chat.component.ts │ │ ├── client-chat-content.ts │ │ ├── gemini.service.ts │ │ ├── line-break.pipe.ts │ │ ├── nx-welcome.component.ts │ │ ├── text │ │ │ ├── text.component.html │ │ │ ├── text.component.scss │ │ │ └── text.component.ts │ │ └── vision │ │ │ ├── vision.component.html │ │ │ ├── vision.component.scss │ │ │ └── vision.component.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── avatar-chatbot.png │ │ └── avatar-user.png │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── styles.scss │ └── test-setup.ts ├── tsconfig.app.json ├── tsconfig.editor.json ├── tsconfig.json └── tsconfig.spec.json ├── images ├── gemini-angular-nestjs.png └── gemini-vision-pro_angular-nestjs-app.png ├── jest.config.ts ├── jest.preset.js ├── libs └── data-model │ ├── .eslintrc.json │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ └── chat-content.ts │ ├── tsconfig.json │ └── tsconfig.lib.json ├── nx.json ├── package-lock.json ├── package.json ├── project.json ├── server ├── .env.example ├── .eslintrc.json ├── jest.config.ts ├── project.json ├── src │ ├── app │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.spec.ts │ │ ├── chat.service.ts │ │ ├── text.service.ts │ │ └── vision.service.ts │ ├── assets │ │ └── .gitkeep │ └── main.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── webpack.config.js ├── tools └── scripts │ └── publish.mjs └── tsconfig.base.json /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.base.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 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["**/*"], 3 | "overrides": [ 4 | { 5 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 6 | "rules": { 7 | "@nx/enforce-module-boundaries": [ 8 | "error", 9 | { 10 | "enforceBuildableLibDependency": true, 11 | "allow": [], 12 | "depConstraints": [ 13 | { 14 | "sourceTag": "*", 15 | "onlyDependOnLibsWithTags": ["*"] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | }, 22 | { 23 | "files": ["*.ts", "*.tsx"], 24 | "rules": {} 25 | }, 26 | { 27 | "files": ["*.js", "*.jsx"], 28 | "rules": {} 29 | }, 30 | { 31 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 32 | "env": { 33 | "jest": true 34 | }, 35 | "rules": {} 36 | } 37 | ], 38 | "extends": ["./.eslintrc.base.json"] 39 | } 40 | -------------------------------------------------------------------------------- /.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 | .angular 43 | 44 | # API Key 45 | .env 46 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | .angular 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.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 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | GitHub stars 4 | 5 | 6 | Tweet 7 | 8 |

9 | 10 | # A Chatbot Web Application using Angular, NestJS and the Gemini API 11 | 12 | This project has been implemented entirely using the `TypeScript` language. 13 | 14 | 15 | 16 | ## Blog Posts 17 | 18 | 1. [https://luixaviles.com/2024/03/build-gemini-chatbot-with-angular-and-nestjs/](https://luixaviles.com/2024/03/build-gemini-chatbot-with-angular-and-nestjs/). Start a project from scratch and generate an Nx-based workspace using Angular and NestJS. The web application support a multi-turn conversation(chatbot) and text generation using Gemini models. 19 | 20 | 1. [https://luixaviles.com/2024/03/using-gemini-pro-vision-image-processing-using-angular-nestjs/](https://luixaviles.com/2024/03/using-gemini-pro-vision-image-processing-using-angular-nestjs/). Add the Image processing ability to the existing application. It uses the Gemini Pro Vision Model. 21 | 22 | ## Features 23 | 24 | This project currently supports: 25 | 26 | - Multi-turn conversations (Chatbot application) 27 | - Text Generation 28 | - Image Processing 29 | 30 | 31 | 32 | ## Support this project 33 | - Star GitHub repository :star: 34 | - Create pull requests, submit bugs or suggest new features 35 | - Follow updates on [Twitter](https://twitter.com/luixaviles) or [Github](https://github.com/luixaviles) 36 | 37 | ## Running the Project Locally 38 | First, ensure you have the following installed: 39 | 40 | 1. NodeJS - Download and Install latest version of Node: [NodeJS](https://nodejs.org) 41 | 2. Git - Download and Install [Git](https://git-scm.com) 42 | 43 | After that, use `Git bash` to run all commands if you are on Windows platform. 44 | 45 | ### Clone repository 46 | In order to start the project use: 47 | 48 | ```bash 49 | $ git clone https://github.com/luixaviles/gemini-angular-nestjs.git 50 | $ cd gemini-angular-nestjs 51 | ``` 52 | ### Get an API Key from Google AI Studio 53 | 54 | Go to the [Google AI Studio](https://aistudio.google.com/app/) website and generate an API Key. 55 | 56 | Next, create an `.env` file under the `/server` directory with the API key value you generated(You'll find a `.env.example` file as an example there): 57 | 58 | ```txt 59 | API_KEY= 60 | ``` 61 | 62 | ### Preview the Application 63 | This project is based on Nx tooling. If you don't have Nx installed, you can do so by using: 64 | 65 | ```bash 66 | npm add --global nx@latest 67 | ``` 68 | 69 | Open other command line window and run following commands: 70 | 71 | ```bash 72 | npm install 73 | nx serve client && nx serve server 74 | ``` 75 | 76 | Then you will need to open your favorite web browser with the following URL: [http://localhost:4200](http://localhost:4200/) 77 | 78 | ## Forks 79 | The Open Source community is awesome! If you're working in a fork with other tech stack, please add the reference of your project here. 80 | 81 | 82 | ## License 83 | MIT 84 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.json", "../.eslintrc.base.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": "corp", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "corp", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /client/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'client', 4 | preset: '../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: '../coverage/client', 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 | -------------------------------------------------------------------------------- /client/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "prefix": "corp", 6 | "sourceRoot": "client/src", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@angular-devkit/build-angular:application", 11 | "outputs": [ 12 | "{options.outputPath}" 13 | ], 14 | "options": { 15 | "outputPath": "dist/client", 16 | "index": "client/src/index.html", 17 | "browser": "client/src/main.ts", 18 | "polyfills": [ 19 | "zone.js" 20 | ], 21 | "tsConfig": "client/tsconfig.app.json", 22 | "inlineStyleLanguage": "scss", 23 | "assets": [ 24 | "client/src/favicon.ico", 25 | "client/src/assets" 26 | ], 27 | "styles": [ 28 | "@angular/material/prebuilt-themes/indigo-pink.css", 29 | "client/src/styles.scss" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "budgets": [ 36 | { 37 | "type": "initial", 38 | "maximumWarning": "500kb", 39 | "maximumError": "1mb" 40 | }, 41 | { 42 | "type": "anyComponentStyle", 43 | "maximumWarning": "2kb", 44 | "maximumError": "4kb" 45 | } 46 | ], 47 | "outputHashing": "all" 48 | }, 49 | "development": { 50 | "optimization": false, 51 | "extractLicenses": false, 52 | "sourceMap": true 53 | } 54 | }, 55 | "defaultConfiguration": "production" 56 | }, 57 | "serve": { 58 | "executor": "@angular-devkit/build-angular:dev-server", 59 | "configurations": { 60 | "production": { 61 | "buildTarget": "client:build:production" 62 | }, 63 | "development": { 64 | "buildTarget": "client:build:development" 65 | } 66 | }, 67 | "defaultConfiguration": "development" 68 | }, 69 | "extract-i18n": { 70 | "executor": "@angular-devkit/build-angular:extract-i18n", 71 | "options": { 72 | "buildTarget": "client:build" 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | chat_bubble 6 | Chat 7 | 8 | 9 | subject 10 | Text Generation 11 | 12 | 13 | image 14 | Vision 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /client/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .drawer-container { 2 | width: auto; 3 | height: 100%; 4 | border: 1px solid rgba(0, 0, 0, 0.5); 5 | } -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import {MatSidenavModule} from '@angular/material/sidenav'; 6 | import {MatListModule} from '@angular/material/list'; 7 | import {MatIconModule } from '@angular/material/icon'; 8 | 9 | @Component({ 10 | standalone: true, 11 | imports: [ 12 | CommonModule, 13 | RouterModule, 14 | MatSidenavModule, 15 | MatListModule, 16 | MatIconModule, 17 | ], 18 | selector: 'corp-root', 19 | templateUrl: './app.component.html', 20 | styleUrl: './app.component.scss', 21 | }) 22 | export class AppComponent { 23 | } 24 | -------------------------------------------------------------------------------- /client/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | import { appRoutes } from './app.routes'; 4 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 5 | import { provideHttpClient } from '@angular/common/http'; 6 | import { MarkdownModule } from 'ngx-markdown'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | provideRouter(appRoutes), 11 | provideAnimationsAsync(), 12 | provideHttpClient(), 13 | importProvidersFrom([ 14 | MarkdownModule.forRoot() 15 | ]) 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | 3 | export const appRoutes: Route[] = [ 4 | { 5 | path: '', 6 | pathMatch: 'full', 7 | redirectTo: 'chat', 8 | }, 9 | { 10 | path: 'chat', 11 | loadComponent: () => 12 | import('./chat/chat.component').then((mod) => mod.ChatComponent), 13 | providers: [], 14 | }, 15 | { 16 | path: 'text', 17 | loadComponent: () => 18 | import('./text/text.component').then((mod) => mod.TextComponent), 19 | }, 20 | { 21 | path: 'vision', 22 | loadComponent: () => 23 | import('./vision/vision.component').then((mod) => mod.VisionComponent), 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /client/src/app/chat/chat.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Welcome to your Gemini ChatBot App
5 | Write a text to start. 6 |

7 |
8 |
13 | 14 |
15 |

20 |
21 |
22 |
23 | 24 | 38 | -------------------------------------------------------------------------------- /client/src/app/chat/chat.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .chat-input { 8 | padding-top: 20px; 9 | width: calc(100% - 48px); 10 | } 11 | 12 | .user { 13 | background-color: white; 14 | } 15 | 16 | .chatbot { 17 | background-color: #e8eaf6; 18 | } 19 | 20 | .chat-footer-container { 21 | display: flex; 22 | align-items: center; 23 | padding: 0 0 0 10px; 24 | } 25 | 26 | .chat-container { 27 | overflow: auto; 28 | padding: 0 10px 0 10px; 29 | height: 100%; 30 | } 31 | 32 | .chat-message { 33 | display: flex; 34 | align-items: flex-start; 35 | padding: 10px; 36 | margin-top: 10px; 37 | border-radius: 10px; 38 | } 39 | 40 | .avatar { 41 | width: 50px; 42 | height: 50px; 43 | border-radius: 50%; 44 | margin-right: 10px; 45 | } 46 | 47 | .message-details { 48 | flex: 1; 49 | align-self: center; 50 | } 51 | 52 | .username { 53 | font-weight: bold; 54 | color: #333; 55 | } 56 | 57 | .message-content { 58 | margin: 5px 0; 59 | color: #666; 60 | } 61 | 62 | .message-container { 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | height: 100%; 67 | } 68 | 69 | .message { 70 | text-align: center; 71 | color: #333; 72 | padding: 20px; 73 | } 74 | 75 | @keyframes fadeIn { 76 | from { 77 | opacity: 0; 78 | } 79 | to { 80 | opacity: 1; 81 | } 82 | } 83 | 84 | .loading { 85 | font-size: 30px; 86 | animation: fadeIn 1s ease-in-out infinite; 87 | } 88 | 89 | .example-container { 90 | width: auto; 91 | height: 100%; 92 | border: 1px solid rgba(0, 0, 0, 0.5); 93 | } 94 | -------------------------------------------------------------------------------- /client/src/app/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | 10 | import { GeminiService } from '../gemini.service'; 11 | import { LineBreakPipe } from '../line-break.pipe'; 12 | import { finalize } from 'rxjs'; 13 | import { ClientChatContent } from '../client-chat-content'; 14 | 15 | 16 | @Component({ 17 | selector: 'corp-chat', 18 | standalone: true, 19 | imports: [ 20 | CommonModule, 21 | MatIconModule, 22 | MatInputModule, 23 | MatButtonModule, 24 | MatFormFieldModule, 25 | FormsModule, 26 | LineBreakPipe, 27 | ], 28 | templateUrl: './chat.component.html', 29 | styleUrls: ['./chat.component.scss'] 30 | }) 31 | export class ChatComponent { 32 | message = ''; 33 | 34 | contents: ClientChatContent[] = []; 35 | 36 | constructor(private geminiService: GeminiService) {} 37 | 38 | sendMessage(message: string): void { 39 | const chatContent: ClientChatContent = { 40 | agent: 'user', 41 | message, 42 | }; 43 | 44 | this.contents.push(chatContent); 45 | this.contents.push({ 46 | agent: 'chatbot', 47 | message: '...', 48 | loading: true, 49 | }); 50 | 51 | this.message = ''; 52 | this.geminiService 53 | .chat(chatContent) 54 | .pipe( 55 | finalize(() => { 56 | const loadingMessageIndex = this.contents.findIndex( 57 | (content) => content.loading 58 | ); 59 | if (loadingMessageIndex !== -1) { 60 | this.contents.splice(loadingMessageIndex, 1); 61 | } 62 | }) 63 | ) 64 | .subscribe((content) => { 65 | this.contents.push(content); 66 | }); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /client/src/app/client-chat-content.ts: -------------------------------------------------------------------------------- 1 | import { ChatContent } from 'data-model'; 2 | 3 | export interface ClientChatContent extends ChatContent { 4 | loading?: boolean; 5 | imagePreview?: string; 6 | } -------------------------------------------------------------------------------- /client/src/app/gemini.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { ClientChatContent } from './client-chat-content'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class GeminiService { 10 | constructor(private httpClient: HttpClient) { } 11 | 12 | chat(chatContent: ClientChatContent): Observable { 13 | return this.httpClient.post('http://localhost:3000/api/chat', chatContent); 14 | } 15 | 16 | generateText(message: string): Observable { 17 | return this.httpClient.post('http://localhost:3000/api/text', {message}); 18 | } 19 | 20 | vision(message: string, file: File): Observable { 21 | const formData = new FormData(); 22 | formData.append('file', file); 23 | formData.append('message', message); 24 | return this.httpClient.post('http://localhost:3000/api/vision', formData); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/app/line-break.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'lineBreak', 5 | standalone: true 6 | }) 7 | export class LineBreakPipe implements PipeTransform { 8 | 9 | transform(value: string, ...args: unknown[]): unknown { 10 | return value.replace(/(?:\r\n|\r|\n)/g, '
'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /client/src/app/nx-welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @Component({ 5 | selector: 'corp-nx-welcome', 6 | standalone: true, 7 | imports: [CommonModule], 8 | template: ` 9 | 16 | 429 |
430 |
431 | 432 |
433 |

434 | Hello there, 435 | Welcome client 👋 436 |

437 |
438 | 439 |
440 |
441 |

442 | 448 | 454 | 455 | You're up and running 456 |

457 | What's next? 458 |
459 |
460 | 466 | 469 | 470 |
471 |
472 | 473 | 796 | 797 |
798 |

Next steps

799 |

Here are some things you can do with Nx:

800 |
801 | 802 | 808 | 814 | 815 | Add UI library 816 | 817 |
# Generate UI lib
818 | nx g @nx/angular:lib ui
819 | # Add a component
820 | nx g @nx/angular:component ui/src/lib/button
821 |
822 |
823 | 824 | 830 | 836 | 837 | View project details 838 | 839 |
nx show project client --web
840 |
841 |
842 | 843 | 849 | 855 | 856 | View interactive project graph 857 | 858 |
nx graph
859 |
860 |
861 | 862 | 868 | 874 | 875 | Run affected commands 876 | 877 |
# see what's been affected by changes
878 | nx affected:graph
879 | # run tests for current changes
880 | nx affected:test
881 | # run e2e tests for current changes
882 | nx affected:e2e
883 |
884 |
885 |

886 | Carefully crafted with 887 | 893 | 899 | 900 |

901 |
902 |
903 | `, 904 | styles: [], 905 | encapsulation: ViewEncapsulation.None, 906 | }) 907 | export class NxWelcomeComponent {} 908 | -------------------------------------------------------------------------------- /client/src/app/text/text.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Welcome to your Gemini App for text generation.
5 | Write an instruction to start. 6 |

7 |
8 |
13 | 14 |
15 |

20 | 24 |
25 |
26 |
27 | 28 | 42 | -------------------------------------------------------------------------------- /client/src/app/text/text.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .chat-input { 8 | padding-top: 20px; 9 | width: calc(100% - 48px); 10 | } 11 | 12 | .user { 13 | background-color: white; 14 | } 15 | 16 | .chatbot { 17 | background-color: #e8eaf6; 18 | } 19 | 20 | .chat-footer-container { 21 | display: flex; 22 | align-items: center; 23 | padding: 0 0 0 10px; 24 | } 25 | 26 | .chat-container { 27 | overflow: auto; 28 | padding: 0 10px 0 10px; 29 | height: 100%; 30 | } 31 | 32 | .chat-message { 33 | display: flex; 34 | align-items: flex-start; 35 | padding: 10px; 36 | margin-top: 10px; 37 | border-radius: 10px; 38 | } 39 | 40 | .avatar { 41 | width: 50px; 42 | height: 50px; 43 | border-radius: 50%; 44 | margin-right: 10px; 45 | } 46 | 47 | .message-details { 48 | flex: 1; 49 | align-self: center; 50 | } 51 | 52 | .username { 53 | font-weight: bold; 54 | color: #333; 55 | } 56 | 57 | .message-content { 58 | margin: 5px 0; 59 | color: #666; 60 | } 61 | 62 | .message-container { 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | height: 100%; 67 | } 68 | 69 | .message { 70 | text-align: center; 71 | color: #333; 72 | padding: 20px; 73 | } 74 | 75 | @keyframes fadeIn { 76 | from { 77 | opacity: 0; 78 | } 79 | to { 80 | opacity: 1; 81 | } 82 | } 83 | 84 | .loading { 85 | font-size: 30px; 86 | animation: fadeIn 1s ease-in-out infinite; 87 | } 88 | 89 | .example-container { 90 | width: auto; 91 | height: 100%; 92 | border: 1px solid rgba(0, 0, 0, 0.5); 93 | } 94 | 95 | .context-container .mat-expansion-panel-header-description { 96 | justify-content: space-between; 97 | align-items: center; 98 | } 99 | -------------------------------------------------------------------------------- /client/src/app/text/text.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | 10 | import { MarkdownModule } from 'ngx-markdown'; 11 | 12 | import { GeminiService } from '../gemini.service'; 13 | import { ClientChatContent } from '../client-chat-content'; 14 | import { LineBreakPipe } from '../line-break.pipe'; 15 | import { finalize } from 'rxjs'; 16 | 17 | 18 | @Component({ 19 | selector: 'corp-text', 20 | standalone: true, 21 | imports: [ 22 | CommonModule, 23 | MatIconModule, 24 | MatInputModule, 25 | MatButtonModule, 26 | MatFormFieldModule, 27 | FormsModule, 28 | LineBreakPipe, 29 | MarkdownModule 30 | ], 31 | templateUrl: './text.component.html', 32 | styleUrls: ['./text.component.scss'] 33 | }) 34 | export class TextComponent { 35 | message = ''; 36 | contents: ClientChatContent[] = []; 37 | 38 | constructor(private geminiService: GeminiService) {} 39 | 40 | generateText(message: string): void { 41 | const chatContent: ClientChatContent = { 42 | agent: 'user', 43 | message, 44 | }; 45 | 46 | this.contents.push(chatContent); 47 | this.contents.push({ 48 | agent: 'chatbot', 49 | message: '...', 50 | loading: true, 51 | }); 52 | 53 | this.message = ''; 54 | this.geminiService 55 | .generateText(message) 56 | .pipe( 57 | finalize(() => { 58 | const loadingMessageIndex = this.contents.findIndex( 59 | (content) => content.loading 60 | ); 61 | if (loadingMessageIndex !== -1) { 62 | this.contents.splice(loadingMessageIndex, 1); 63 | } 64 | }) 65 | ) 66 | .subscribe((content) => { 67 | this.contents.push(content); 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /client/src/app/vision/vision.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Welcome to your Gemini Vision App
5 | Write a text and attach an image to start. 6 |

7 |
8 |
13 | 14 |
15 | Image Preview 16 |

21 |
22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /client/src/app/vision/vision.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .chat-input { 8 | padding-top: 20px; 9 | width: calc(100% - 48px); 10 | } 11 | 12 | .user { 13 | background-color: white; 14 | } 15 | 16 | .chatbot { 17 | background-color: #e8eaf6; 18 | } 19 | 20 | .chat-footer-container { 21 | display: flex; 22 | align-items: center; 23 | padding: 0 0 0 10px; 24 | } 25 | 26 | .chat-container { 27 | overflow: auto; 28 | padding: 0 10px 0 10px; 29 | height: 100%; 30 | } 31 | 32 | .chat-message { 33 | display: flex; 34 | align-items: flex-start; 35 | padding: 10px; 36 | margin-top: 10px; 37 | border-radius: 10px; 38 | } 39 | 40 | .avatar { 41 | width: 50px; 42 | height: 50px; 43 | border-radius: 50%; 44 | margin-right: 10px; 45 | } 46 | 47 | .message-details { 48 | flex: 1; 49 | align-self: center; 50 | } 51 | 52 | .username { 53 | font-weight: bold; 54 | color: #333; 55 | } 56 | 57 | .message-content { 58 | margin: 5px 0; 59 | color: #666; 60 | } 61 | 62 | .message-container { 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | height: 100%; 67 | } 68 | 69 | .message { 70 | text-align: center; 71 | color: #333; 72 | padding: 20px; 73 | } 74 | 75 | @keyframes fadeIn { 76 | from { 77 | opacity: 0; 78 | } 79 | to { 80 | opacity: 1; 81 | } 82 | } 83 | 84 | .loading { 85 | font-size: 30px; 86 | animation: fadeIn 1s ease-in-out infinite; 87 | } 88 | 89 | .example-container { 90 | width: auto; 91 | height: 100%; 92 | border: 1px solid rgba(0, 0, 0, 0.5); 93 | } 94 | 95 | .image-upload-button { 96 | input[type="file"] { 97 | font-size: 100px; 98 | left: 0; 99 | opacity: 0; 100 | position: absolute; 101 | top: 0; 102 | z-index: 1; 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /client/src/app/vision/vision.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | 10 | import { GeminiService } from '../gemini.service'; 11 | import { LineBreakPipe } from '../line-break.pipe'; 12 | import { EMPTY, catchError, finalize } from 'rxjs'; 13 | import { ClientChatContent } from '../client-chat-content'; 14 | 15 | type ImageFile = { preview: string; file: File }; 16 | 17 | @Component({ 18 | selector: 'corp-vision', 19 | standalone: true, 20 | imports: [ 21 | CommonModule, 22 | MatIconModule, 23 | MatInputModule, 24 | MatButtonModule, 25 | MatFormFieldModule, 26 | FormsModule, 27 | LineBreakPipe, 28 | ], 29 | templateUrl: './vision.component.html', 30 | styleUrl: './vision.component.scss', 31 | }) 32 | export class VisionComponent { 33 | message = ''; 34 | contents: ClientChatContent[] = []; 35 | imageFile: ImageFile | undefined; 36 | 37 | constructor(private geminiService: GeminiService) {} 38 | 39 | sendMessage(message: string): void { 40 | if(!this.imageFile) { 41 | return; 42 | } 43 | 44 | const chatContent: ClientChatContent = { 45 | agent: 'user', 46 | message, 47 | imagePreview: this.imageFile?.preview 48 | }; 49 | const file = this.imageFile.file; 50 | 51 | this.contents.push(chatContent); 52 | this.contents.push({ 53 | agent: 'chatbot', 54 | message: '...', 55 | loading: true, 56 | }); 57 | 58 | this.message = ''; 59 | this.imageFile = undefined; 60 | 61 | this.geminiService 62 | .vision(chatContent.message, file) 63 | .pipe( 64 | catchError(() => { 65 | return EMPTY; 66 | }), 67 | finalize(() => { 68 | const loadingMessageIndex = this.contents.findIndex( 69 | (content) => content.loading 70 | ); 71 | if (loadingMessageIndex !== -1) { 72 | this.contents.splice(loadingMessageIndex, 1); 73 | } 74 | }) 75 | ) 76 | .subscribe((content) => { 77 | this.contents.push(content); 78 | }); 79 | } 80 | 81 | selectImage(event: Event) { 82 | const inputElement = event.target as HTMLInputElement; 83 | const file = inputElement.files?.item(0); 84 | if (file) { 85 | const reader = new FileReader(); 86 | reader.onload = (e: ProgressEvent) => { 87 | const preview = e.target?.result as string; 88 | this.imageFile = {file, preview}; 89 | }; 90 | 91 | reader.readAsDataURL(file); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/src/assets/avatar-chatbot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/client/src/assets/avatar-chatbot.png -------------------------------------------------------------------------------- /client/src/assets/avatar-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/client/src/assets/avatar-user.png -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/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 | -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /client/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 2 | globalThis.ngJest = { 3 | testEnvironmentOptions: { 4 | errorOnUnknownElements: true, 5 | errorOnUnknownProperties: true, 6 | }, 7 | }; 8 | import 'jest-preset-angular/setup-jest'; 9 | -------------------------------------------------------------------------------- /client/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 | -------------------------------------------------------------------------------- /client/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": {}, 5 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true 12 | }, 13 | "files": [], 14 | "include": [], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.editor.json" 18 | }, 19 | { 20 | "path": "./tsconfig.app.json" 21 | }, 22 | { 23 | "path": "./tsconfig.spec.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 | -------------------------------------------------------------------------------- /client/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 | -------------------------------------------------------------------------------- /images/gemini-angular-nestjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/images/gemini-angular-nestjs.png -------------------------------------------------------------------------------- /images/gemini-vision-pro_angular-nestjs-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/images/gemini-vision-pro_angular-nestjs-app.png -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/data-model/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.base.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": "error" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /libs/data-model/README.md: -------------------------------------------------------------------------------- 1 | # data-model 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build data-model` to build the library. 8 | -------------------------------------------------------------------------------- /libs/data-model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-model", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "tslib": "^2.3.0" 6 | }, 7 | "type": "commonjs", 8 | "main": "./src/index.js", 9 | "typings": "./src/index.d.ts" 10 | } 11 | -------------------------------------------------------------------------------- /libs/data-model/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-model", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/data-model/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/data-model", 12 | "main": "libs/data-model/src/index.ts", 13 | "tsConfig": "libs/data-model/tsconfig.lib.json", 14 | "assets": ["libs/data-model/*.md"] 15 | } 16 | }, 17 | "publish": { 18 | "command": "node tools/scripts/publish.mjs data-model {args.ver} {args.tag}", 19 | "dependsOn": ["build"] 20 | } 21 | }, 22 | "tags": [] 23 | } 24 | -------------------------------------------------------------------------------- /libs/data-model/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/chat-content'; 2 | -------------------------------------------------------------------------------- /libs/data-model/src/lib/chat-content.ts: -------------------------------------------------------------------------------- 1 | export interface ChatContent { 2 | agent: 'user' | 'chatbot'; 3 | message: string; 4 | } -------------------------------------------------------------------------------- /libs/data-model/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 | } 20 | -------------------------------------------------------------------------------- /libs/data-model/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "namedInputs": { 4 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 5 | "production": [ 6 | "default", 7 | "!{projectRoot}/.eslintrc.json", 8 | "!{projectRoot}/eslint.config.js", 9 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 10 | "!{projectRoot}/tsconfig.spec.json", 11 | "!{projectRoot}/jest.config.[jt]s", 12 | "!{projectRoot}/src/test-setup.[jt]s", 13 | "!{projectRoot}/test-setup.[jt]s" 14 | ], 15 | "sharedGlobals": [] 16 | }, 17 | "targetDefaults": { 18 | "@angular-devkit/build-angular:application": { 19 | "cache": true, 20 | "dependsOn": ["^build"], 21 | "inputs": ["production", "^production"] 22 | }, 23 | "@nx/js:tsc": { 24 | "cache": true, 25 | "dependsOn": ["^build"], 26 | "inputs": ["production", "^production"] 27 | } 28 | }, 29 | "plugins": [ 30 | { 31 | "plugin": "@nx/eslint/plugin", 32 | "options": { 33 | "targetName": "lint" 34 | } 35 | }, 36 | { 37 | "plugin": "@nx/jest/plugin", 38 | "options": { 39 | "targetName": "test" 40 | } 41 | }, 42 | { 43 | "plugin": "@nx/webpack/plugin", 44 | "options": { 45 | "buildTargetName": "build", 46 | "serveTargetName": "serve", 47 | "previewTargetName": "preview" 48 | } 49 | } 50 | ], 51 | "generators": { 52 | "@nx/angular:application": { 53 | "e2eTestRunner": "none", 54 | "linter": "eslint", 55 | "style": "scss", 56 | "unitTestRunner": "jest" 57 | }, 58 | "@nx/angular:component": { 59 | "style": "css" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gemini-angular-nestjs/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": {}, 6 | "private": true, 7 | "devDependencies": { 8 | "@angular-devkit/build-angular": "~17.1.0", 9 | "@angular-devkit/core": "~17.1.0", 10 | "@angular-devkit/schematics": "~17.1.0", 11 | "@angular-eslint/eslint-plugin": "~17.0.0", 12 | "@angular-eslint/eslint-plugin-template": "~17.0.0", 13 | "@angular-eslint/template-parser": "~17.0.0", 14 | "@angular/cli": "~17.1.0", 15 | "@angular/compiler-cli": "~17.1.0", 16 | "@angular/language-service": "~17.1.0", 17 | "@nestjs/schematics": "^10.0.1", 18 | "@nestjs/testing": "^10.0.2", 19 | "@nx/angular": "^18.0.5", 20 | "@nx/eslint": "18.0.5", 21 | "@nx/eslint-plugin": "18.0.5", 22 | "@nx/jest": "18.0.5", 23 | "@nx/js": "18.0.5", 24 | "@nx/nest": "^18.0.5", 25 | "@nx/node": "18.0.5", 26 | "@nx/web": "18.0.5", 27 | "@nx/webpack": "18.0.5", 28 | "@nx/workspace": "18.0.5", 29 | "@schematics/angular": "~17.1.0", 30 | "@swc-node/register": "~1.8.0", 31 | "@swc/core": "~1.3.85", 32 | "@swc/helpers": "~0.5.2", 33 | "@types/jest": "^29.4.0", 34 | "@types/multer": "^1.4.11", 35 | "@types/node": "18.16.9", 36 | "@typescript-eslint/eslint-plugin": "^6.13.2", 37 | "@typescript-eslint/parser": "^6.13.2", 38 | "eslint": "~8.48.0", 39 | "eslint-config-prettier": "^9.0.0", 40 | "jest": "^29.4.1", 41 | "jest-environment-jsdom": "^29.4.1", 42 | "jest-environment-node": "^29.4.1", 43 | "jest-preset-angular": "~13.1.4", 44 | "jsonc-eslint-parser": "^2.1.0", 45 | "nx": "18.0.5", 46 | "prettier": "^2.6.2", 47 | "ts-jest": "^29.1.0", 48 | "ts-node": "10.9.1", 49 | "typescript": "~5.3.2", 50 | "verdaccio": "^5.0.4", 51 | "webpack-cli": "^5.1.4" 52 | }, 53 | "dependencies": { 54 | "@angular/animations": "~17.1.0", 55 | "@angular/cdk": "^17.2.1", 56 | "@angular/common": "~17.1.0", 57 | "@angular/compiler": "~17.1.0", 58 | "@angular/core": "~17.1.0", 59 | "@angular/forms": "~17.1.0", 60 | "@angular/material": "^17.2.1", 61 | "@angular/platform-browser": "~17.1.0", 62 | "@angular/platform-browser-dynamic": "~17.1.0", 63 | "@angular/router": "~17.1.0", 64 | "@google/generative-ai": "^0.2.1", 65 | "@nestjs/common": "^10.0.2", 66 | "@nestjs/config": "^3.2.0", 67 | "@nestjs/core": "^10.0.2", 68 | "@nestjs/platform-express": "^10.0.2", 69 | "dotenv": "16.4.5", 70 | "marked": "~9.1.6", 71 | "ngx-markdown": "~17.1.1", 72 | "reflect-metadata": "^0.1.13", 73 | "rxjs": "~7.8.0", 74 | "tslib": "^2.3.0", 75 | "zone.js": "~0.14.3" 76 | }, 77 | "nx": { 78 | "includedScripts": [] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gemini-angular-nestjs/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 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | API_KEY= 2 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.json", "../.eslintrc.base.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 | -------------------------------------------------------------------------------- /server/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'server', 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/server', 11 | }; 12 | -------------------------------------------------------------------------------- /server/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "server/src", 5 | "projectType": "application", 6 | "targets": { 7 | "serve": { 8 | "executor": "@nx/js:node", 9 | "defaultConfiguration": "development", 10 | "options": { 11 | "buildTarget": "server:build" 12 | }, 13 | "configurations": { 14 | "development": { 15 | "buildTarget": "server:build:development" 16 | }, 17 | "production": { 18 | "buildTarget": "server:build:production" 19 | } 20 | } 21 | } 22 | }, 23 | "tags": [] 24 | } 25 | -------------------------------------------------------------------------------- /server/src/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let app: TestingModule; 8 | 9 | beforeAll(async () => { 10 | app = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | }); 15 | 16 | describe('getData', () => { 17 | it('should return "Hello API"', () => { 18 | const appController = app.get(AppController); 19 | expect(appController.getData()).toEqual({ message: 'Hello API' }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /server/src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, UseInterceptors, UploadedFile } from '@nestjs/common'; 2 | import { FileInterceptor } from '@nestjs/platform-express'; 3 | import { Express } from 'express'; 4 | import Multer from 'multer'; 5 | 6 | import { ChatContent } from 'data-model'; 7 | import { ChatService } from './chat.service'; 8 | import { TextService } from './text.service'; 9 | import { VisionService } from './vision.service'; 10 | 11 | @Controller() 12 | export class AppController { 13 | 14 | constructor(private readonly chatService: ChatService, 15 | private readonly textService: TextService, 16 | private readonly visionService: VisionService) {} 17 | 18 | @Post('chat') 19 | chat(@Body() chatContent: ChatContent) { 20 | return this.chatService.chat(chatContent); 21 | } 22 | 23 | @Post('text') 24 | text(@Body() chatContent: ChatContent) { 25 | return this.textService.generateText(chatContent.message); 26 | } 27 | 28 | @Post('vision') 29 | @UseInterceptors(FileInterceptor('file')) 30 | uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body: {message: string}) { 31 | return this.visionService.vision(body.message, file); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | 4 | import { AppController } from './app.controller'; 5 | import { ChatService } from './chat.service'; 6 | import { TextService } from './text.service'; 7 | import { VisionService } from './vision.service'; 8 | 9 | @Module({ 10 | imports: [ConfigModule.forRoot()], 11 | controllers: [AppController], 12 | providers: [ChatService, TextService, VisionService], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /server/src/app/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppService', () => { 6 | let service: AppService; 7 | 8 | beforeAll(async () => { 9 | const app = await Test.createTestingModule({ 10 | providers: [AppService], 11 | }).compile(); 12 | 13 | service = app.get(AppService); 14 | }); 15 | 16 | describe('getData', () => { 17 | it('should return "Hello API"', () => { 18 | expect(service.getData()).toEqual({ message: 'Hello API' }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /server/src/app/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | ChatSession, 4 | GenerativeModel, 5 | GoogleGenerativeAI, 6 | } from '@google/generative-ai'; 7 | 8 | import { ChatContent } from 'data-model'; 9 | 10 | @Injectable() 11 | export class ChatService { 12 | model: GenerativeModel; 13 | chatSession: ChatSession; 14 | constructor() { 15 | const genAI = new GoogleGenerativeAI(process.env.API_KEY); 16 | this.model = genAI.getGenerativeModel({ model: 'gemini-pro' }); 17 | this.chatSession = this.model.startChat({ 18 | history: [ 19 | { 20 | role: 'user', 21 | parts: `You're a poet. Respond to all questions with a rhyming poem. 22 | What is the capital of California? 23 | `, 24 | }, 25 | { 26 | role: 'model', 27 | parts: 28 | 'If the capital of California is what you seek, Sacramento is where you ought to peek.', 29 | }, 30 | ], 31 | }); 32 | } 33 | 34 | async chat(chatContent: ChatContent): Promise { 35 | const result = await this.chatSession.sendMessage(chatContent.message); 36 | const response = await result.response; 37 | const text = response.text(); 38 | 39 | return { 40 | message: text, 41 | agent: 'chatbot', 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/src/app/text.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | GenerativeModel, 4 | GoogleGenerativeAI, 5 | } from '@google/generative-ai'; 6 | import { ChatContent } from 'data-model'; 7 | 8 | @Injectable() 9 | export class TextService { 10 | model: GenerativeModel; 11 | 12 | constructor() { 13 | const genAI = new GoogleGenerativeAI(process.env.API_KEY); 14 | this.model = genAI.getGenerativeModel({ model: "gemini-pro"}); 15 | } 16 | 17 | async generateText(message: string): Promise { 18 | const result = await this.model.generateContent(message); 19 | const response = await result.response; 20 | const text = response.text(); 21 | 22 | return { 23 | message: text, 24 | agent: 'chatbot', 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/app/vision.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | GenerativeModel, 4 | GoogleGenerativeAI, 5 | InlineDataPart, 6 | } from '@google/generative-ai'; 7 | import { ChatContent } from 'data-model'; 8 | 9 | @Injectable() 10 | export class VisionService { 11 | model: GenerativeModel; 12 | 13 | constructor() { 14 | const genAI = new GoogleGenerativeAI(process.env.API_KEY); 15 | this.model = genAI.getGenerativeModel({ model: "gemini-pro-vision"}); 16 | } 17 | 18 | async vision(message: string, file: Express.Multer.File): Promise { 19 | const imageDataPart: InlineDataPart = { 20 | inlineData: { 21 | data: file.buffer.toString('base64'), 22 | mimeType: file.mimetype, 23 | }, 24 | }; 25 | const result = await this.model.generateContent([message, imageDataPart]); 26 | const response = await result.response; 27 | const text = response.text(); 28 | 29 | return { 30 | message: text, 31 | agent: 'chatbot', 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luixaviles/gemini-angular-nestjs/a3fe68677b87008abb716dbb55484b70354c3fcb/server/src/assets/.gitkeep -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | */ 5 | 6 | import { Logger } from '@nestjs/common'; 7 | import { NestFactory } from '@nestjs/core'; 8 | 9 | import { AppModule } from './app/app.module'; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule); 13 | const globalPrefix = 'api'; 14 | app.setGlobalPrefix(globalPrefix); 15 | app.enableCors(); 16 | const port = process.env.PORT || 3000; 17 | await app.listen(port); 18 | Logger.log( 19 | `🚀 Application is running on: http://localhost:${port}/${globalPrefix}` 20 | ); 21 | } 22 | 23 | bootstrap(); 24 | -------------------------------------------------------------------------------- /server/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 | -------------------------------------------------------------------------------- /server/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 | "types": ["Multer"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/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 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { NxWebpackPlugin } = require('@nx/webpack'); 2 | const { join } = require('path'); 3 | 4 | module.exports = { 5 | output: { 6 | path: join(__dirname, '../dist/server'), 7 | }, 8 | plugins: [ 9 | new NxWebpackPlugin({ 10 | target: 'node', 11 | compiler: 'tsc', 12 | main: './src/main.ts', 13 | tsConfig: './tsconfig.app.json', 14 | assets: ['./src/assets'], 15 | optimization: false, 16 | outputHashing: 'none', 17 | }), 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /tools/scripts/publish.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a minimal script to publish your package to "npm". 3 | * This is meant to be used as-is or customize as you see fit. 4 | * 5 | * This script is executed on "dist/path/to/library" as "cwd" by default. 6 | * 7 | * You might need to authenticate with NPM before running this script. 8 | */ 9 | 10 | import { execSync } from 'child_process'; 11 | import { readFileSync, writeFileSync } from 'fs'; 12 | 13 | import devkit from '@nx/devkit'; 14 | const { readCachedProjectGraph } = devkit; 15 | 16 | function invariant(condition, message) { 17 | if (!condition) { 18 | console.error(message); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | // Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag} 24 | // Default "tag" to "next" so we won't publish the "latest" tag by accident. 25 | const [, , name, version, tag = 'next'] = process.argv; 26 | 27 | // A simple SemVer validation to validate the version 28 | const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/; 29 | invariant( 30 | version && validVersion.test(version), 31 | `No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.` 32 | ); 33 | 34 | const graph = readCachedProjectGraph(); 35 | const project = graph.nodes[name]; 36 | 37 | invariant( 38 | project, 39 | `Could not find project "${name}" in the workspace. Is the project.json configured correctly?` 40 | ); 41 | 42 | const outputPath = project.data?.targets?.build?.options?.outputPath; 43 | invariant( 44 | outputPath, 45 | `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?` 46 | ); 47 | 48 | process.chdir(outputPath); 49 | 50 | // Updating the version in "package.json" before publishing 51 | try { 52 | const json = JSON.parse(readFileSync(`package.json`).toString()); 53 | json.version = version; 54 | writeFileSync(`package.json`, JSON.stringify(json, null, 2)); 55 | } catch (e) { 56 | console.error(`Error reading package.json file from library build output.`); 57 | } 58 | 59 | // Execute "npm publish" to publish 60 | execSync(`npm publish --access public --tag ${tag}`); 61 | -------------------------------------------------------------------------------- /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 | "data-model": ["libs/data-model/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | --------------------------------------------------------------------------------