diff --git a/bizmatch-server/package.json b/bizmatch-server/package.json index aa58df4..ecdb4b5 100644 --- a/bizmatch-server/package.json +++ b/bizmatch-server/package.json @@ -36,7 +36,7 @@ "@nestjs/serve-static": "^4.0.1", "cors": "^2.8.5", "dotenv": "^16.4.5", - "drizzle-orm": "^0.30.8", + "drizzle-orm": "^0.32.0", "fs-extra": "^11.2.0", "handlebars": "^4.7.8", "jwks-rsa": "^3.1.0", @@ -44,13 +44,13 @@ "nest-winston": "^1.9.4", "nodemailer": "^6.9.10", "nodemailer-smtp-transport": "^2.7.4", + "openai": "^4.52.6", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.11.5", - "redis": "^4.6.13", - "redis-om": "^0.4.3", + "pgvector": "^0.2.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.33.2", @@ -77,7 +77,7 @@ "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "commander": "^12.0.0", - "drizzle-kit": "^0.20.16", + "drizzle-kit": "^0.23.0", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/bizmatch-server/src/drizzle/import.ts b/bizmatch-server/src/drizzle/import.ts index 093163d..f1c8b05 100644 --- a/bizmatch-server/src/drizzle/import.ts +++ b/bizmatch-server/src/drizzle/import.ts @@ -2,6 +2,7 @@ import 'dotenv/config'; import { drizzle } from 'drizzle-orm/node-postgres'; import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs'; import fs from 'fs-extra'; +import OpenAI from 'openai'; import { join } from 'path'; import pkg from 'pg'; import { rimraf } from 'rimraf'; @@ -11,6 +12,10 @@ import { emailToDirName } from 'src/models/main.model.js'; import * as schema from './schema.js'; const { Pool } = pkg; +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen +}); + const connectionString = process.env.DATABASE_URL; // const pool = new Pool({connectionString}) const client = new Pool({ connectionString }); @@ -124,6 +129,14 @@ for (const commercial of commercialJsonData) { //End await client.end(); +async function createEmbedding(text: string): Promise { + const response = await openai.embeddings.create({ + model: 'text-embedding-ada-002', + input: text, + }); + return response.data[0].embedding; +} + function getRandomItem(arr: T[]): T { if (arr.length === 0) { throw new Error('The array is empty.'); diff --git a/bizmatch-server/src/drizzle/schema.ts b/bizmatch-server/src/drizzle/schema.ts index c2eb5e2..7958f2d 100644 --- a/bizmatch-server/src/drizzle/schema.ts +++ b/bizmatch-server/src/drizzle/schema.ts @@ -1,6 +1,5 @@ -import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar, vector } from 'drizzle-orm/pg-core'; import { AreasServed, LicensedIn } from 'src/models/db.model'; - export const PG_CONNECTION = 'PG_CONNECTION'; export const genderEnum = pgEnum('gender', ['male', 'female']); export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']); @@ -58,6 +57,9 @@ export const businesses = pgTable('businesses', { updated: timestamp('updated'), visits: integer('visits'), lastVisit: timestamp('lastVisit'), + // Neue Spalte für das OpenAI Embedding + embedding: vector('embedding', { dimensions: 1536 }), + // embedding: sql`vector(1536)`, }); export const commercials = pgTable('commercials', { diff --git a/bizmatch/src/app/app.component.html b/bizmatch/src/app/app.component.html index a676a41..1364f24 100644 --- a/bizmatch/src/app/app.component.html +++ b/bizmatch/src/app/app.component.html @@ -30,3 +30,4 @@ } + diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index 9d1e9e5..35e7fd7 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -10,13 +10,14 @@ import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model' import build from '../build'; import { FooterComponent } from './components/footer/footer.component'; import { HeaderComponent } from './components/header/header.component'; +import { MessageContainerComponent } from './components/message/message-container.component'; import { LoadingService } from './services/loading.service'; import { UserService } from './services/user.service'; import { createDefaultListingCriteria } from './utils/utils'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent], + imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.scss', diff --git a/bizmatch/src/app/components/header/header.component.html b/bizmatch/src/app/components/header/header.component.html index 263b4ba..96bdacf 100644 --- a/bizmatch/src/app/components/header/header.component.html +++ b/bizmatch/src/app/components/header/header.component.html @@ -41,19 +41,21 @@ @@ -93,6 +95,7 @@ [ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/businessListings') }" class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700" aria-current="page" + (click)="closeMenus()" >Businesses @@ -102,6 +105,7 @@ routerLink="/commercialPropertyListings" [ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }" class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700" + (click)="closeMenus()" >Properties @@ -111,6 +115,7 @@ routerLink="/brokerListings" [ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }" class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700" + (click)="closeMenus()" >Professionals diff --git a/bizmatch/src/app/components/header/header.component.ts b/bizmatch/src/app/components/header/header.component.ts index 5a41eaf..33d535b 100644 --- a/bizmatch/src/app/components/header/header.component.ts +++ b/bizmatch/src/app/components/header/header.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { Router, RouterModule } from '@angular/router'; import { faUserGear } from '@fortawesome/free-solid-svg-icons'; -import { initFlowbite } from 'flowbite'; +import { Collapse, Dropdown, initFlowbite } from 'flowbite'; import { KeycloakService } from 'keycloak-angular'; import { Observable } from 'rxjs'; import { User } from '../../../../../bizmatch-server/src/models/db.model'; @@ -60,4 +60,26 @@ export class HeaderComponent { isActive(route: string): boolean { return this.router.url === route; } + closeDropdown() { + const dropdownButton = document.getElementById('user-menu-button'); + const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown'); + + if (dropdownButton && dropdownMenu) { + const dropdown = new Dropdown(dropdownMenu, dropdownButton); + dropdown.hide(); + } + } + closeMobileMenu() { + const targetElement = document.getElementById('navbar-user'); + const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]'); + + if (targetElement instanceof HTMLElement && triggerElement instanceof HTMLElement) { + const collapse = new Collapse(targetElement, triggerElement); + collapse.collapse(); + } + } + closeMenus() { + this.closeDropdown(); + this.closeMobileMenu(); + } } diff --git a/bizmatch/src/app/components/message/message-container.component.ts b/bizmatch/src/app/components/message/message-container.component.ts new file mode 100644 index 0000000..031fbf7 --- /dev/null +++ b/bizmatch/src/app/components/message/message-container.component.ts @@ -0,0 +1,30 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { MessageComponent } from './message.component'; +import { Message, MessageService } from './message.service'; + +@Component({ + selector: 'app-message-container', + standalone: true, + imports: [CommonModule, MessageComponent], + template: ` +
+ +
+ `, +}) +export class MessageContainerComponent implements OnInit { + messages: Message[] = []; + + constructor(private messageService: MessageService) {} + + ngOnInit(): void { + this.messageService.messages$.subscribe(messages => { + this.messages = messages; + }); + } + + removeMessage(message: Message): void { + this.messageService.removeMessage(message); + } +} diff --git a/bizmatch/src/app/components/message/message.component.ts b/bizmatch/src/app/components/message/message.component.ts index 6d8eba3..ab87322 100644 --- a/bizmatch/src/app/components/message/message.component.ts +++ b/bizmatch/src/app/components/message/message.component.ts @@ -1,32 +1,25 @@ -import { Component } from '@angular/core'; - -import { AsyncPipe, NgIf } from '@angular/common'; -import { MessageService } from './message.service'; +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Message } from './message.service'; @Component({ selector: 'app-message', standalone: true, - imports: [AsyncPipe, NgIf], + imports: [CommonModule], template: ` - - + @if (isProfessional){
@@ -178,7 +182,7 @@ [Add more licenses or remove existing ones.]
- + }
- } @else { - - } - - } -
- diff --git a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts index dd1ed6e..fde31f7 100644 --- a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts @@ -15,6 +15,7 @@ import { NgxCurrencyDirective } from 'ngx-currency'; import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; +import { MessageService } from '../../../components/message/message.service'; import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { GeoService } from '../../../services/geo.service'; import { ImageService } from '../../../services/image.service'; @@ -39,25 +40,7 @@ export class EditBusinessListingComponent { listing: BusinessListing; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; user: User; - maxFileSize = 3000000; environment = environment; - responsiveOptions = [ - { - breakpoint: '1199px', - numVisible: 1, - numScroll: 1, - }, - { - breakpoint: '991px', - numVisible: 2, - numScroll: 1, - }, - { - breakpoint: '767px', - numVisible: 1, - numScroll: 1, - }, - ]; config = { aspectRatio: 16 / 9 }; editorModules = TOOLBAR_OPTIONS; draggedImage: ImageProperty; @@ -76,7 +59,7 @@ export class EditBusinessListingComponent { private geoService: GeoService, private imageService: ImageService, private loadingService: LoadingService, - + private messageService: MessageService, private route: ActivatedRoute, private keycloakService: KeycloakService, ) { @@ -114,7 +97,7 @@ export class EditBusinessListingComponent { async save() { this.listing = await this.listingsService.save(this.listing, this.listing.listingsCategory); this.router.navigate(['editBusinessListing', this.listing.id]); - // this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 }); + this.messageService.addMessage({ severity: 'success', text: 'Listing changes have been persisted', duration: 3000 }); } suggestions: string[] | undefined; diff --git a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts index 32d2f85..0fb1a0e 100644 --- a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.ts @@ -137,7 +137,7 @@ export class EditCommercialPropertyListingComponent { async save() { this.listing = await this.listingsService.save(this.listing, this.listing.listingsCategory); this.router.navigate(['editCommercialPropertyListing', this.listing.id]); - // this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 }); + this.messageService.addMessage({ severity: 'success', text: 'Listing changes have been persisted', duration: 3000 }); } async search(event: AutoCompleteCompleteEvent) { @@ -167,10 +167,7 @@ export class EditCommercialPropertyListingComponent { if (this.croppedImage) { this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe( async () => { - //console.log('Upload successful', response); - //setTimeout(async () => { this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty')); - //}, 10); this.closeModal(); }, error => { @@ -184,11 +181,10 @@ export class EditCommercialPropertyListingComponent { const confirmed = await this.confirmationService.showConfirmation('Are you sure you want to delete this image?'); if (confirmed) { this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName); - // await Promise.all([, ]); await this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName); await this.listingsService.save(this.listing, 'commercialProperty'); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty')); - this.messageService.showMessage('Image deleted'); + this.messageService.addMessage({ severity: 'success', text: 'Image has been deleted', duration: 3000 }); } else { console.log('deny'); } diff --git a/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.html b/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.html index 3d82819..61a77f7 100644 --- a/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.html +++ b/bizmatch/src/app/pages/subscription/my-listing/my-listing.component.html @@ -88,7 +88,6 @@ -