├── .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 |
4 |
5 |
6 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------