diff --git a/bizmatch-server/package.json b/bizmatch-server/package.json index 5d22c4e..d2182ac 100644 --- a/bizmatch-server/package.json +++ b/bizmatch-server/package.json @@ -38,6 +38,7 @@ "dotenv": "^16.4.5", "drizzle-orm": "^0.32.0", "fs-extra": "^11.2.0", + "groq-sdk": "^0.5.0", "handlebars": "^4.7.8", "jwks-rsa": "^3.1.0", "ky": "^1.4.0", diff --git a/bizmatch-server/src/ai/ai.controller.ts b/bizmatch-server/src/ai/ai.controller.ts new file mode 100644 index 0000000..9e09e40 --- /dev/null +++ b/bizmatch-server/src/ai/ai.controller.ts @@ -0,0 +1,12 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { AiService } from './ai.service.js'; + +@Controller('ai') +export class AiController { + constructor(private readonly aiService: AiService) {} + + @Post() + async getBusinessCriteria(@Body('query') query: string) { + return this.aiService.getBusinessCriteria(query); + } +} diff --git a/bizmatch-server/src/ai/ai.module.ts b/bizmatch-server/src/ai/ai.module.ts new file mode 100644 index 0000000..5360cc5 --- /dev/null +++ b/bizmatch-server/src/ai/ai.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AiController } from './ai.controller.js'; +import { AiService } from './ai.service.js'; + +@Module({ + controllers: [AiController], + providers: [AiService], +}) +export class AiModule {} diff --git a/bizmatch-server/src/ai/ai.service.ts b/bizmatch-server/src/ai/ai.service.ts new file mode 100644 index 0000000..01abffb --- /dev/null +++ b/bizmatch-server/src/ai/ai.service.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@nestjs/common'; +import Groq from 'groq-sdk'; +import OpenAI from 'openai'; +import { BusinessListingCriteria } from '../models/main.model'; + +const businessListingCriteriaStructure = { + criteriaType: 'business | commercialProperty | broker', + types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'", + city: 'string', + state: 'string', + county: 'string', + minPrice: 'number', + maxPrice: 'number', + minRevenue: 'number', + maxRevenue: 'number', + minCashFlow: 'number', + maxCashFlow: 'number', + minNumberEmployees: 'number', + maxNumberEmployees: 'number', + establishedSince: 'number', + establishedUntil: 'number', + realEstateChecked: 'boolean', + leasedLocation: 'boolean', + franchiseResale: 'boolean', + title: 'string', + brokerName: 'string', + searchType: "'exact' | 'radius'", + radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'", +}; +@Injectable() +export class AiService { + private readonly openai: OpenAI; + private readonly groq: Groq; + constructor() { + this.openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, // Verwenden Sie Umgebungsvariablen für den API-Schlüssel + }); + this.groq = new Groq({ apiKey: process.env.GROQ_API_KEY }); + } + + async getBusinessCriteria(query: string): Promise { + // const prompt = ` + // Dieses Objekt ist wie folgt definiert: ${JSON.stringify(businessListingCriteriaStructure)}. + // Die Antwort darf nur das von dir befüllte JSON als unformatierten Text enthalten so das es von mir mit JSON.parse() einlesbar ist!!!! + // Falls es Ortsangaben gibt, dann befülle City, County und State wenn möglich Die Suchanfrage des Users lautet: "${query}"`; + const prompt = `The Search Query of the User is: "${query}"`; + let response = null; + try { + // response = await this.openai.chat.completions.create({ + // model: 'gpt-4o-mini', + // //model: 'gpt-3.5-turbo', + // max_tokens: 300, + // messages: [ + // { + // role: 'system', + // content: `Please create unformatted JSON Object from a user input. + // The type is: ${JSON.stringify(businessListingCriteriaStructure)}., + // If location details available please fill city, county and state as State Code`, + // }, + // ], + // temperature: 0.5, + // response_format: { type: 'json_object' }, + // }); + + response = await this.groq.chat.completions.create({ + messages: [ + { + role: 'system', + content: `Please create unformatted JSON Object from a user input. + The type must be: ${JSON.stringify(businessListingCriteriaStructure)}. + If location details available please fill city, county and state as State Code`, + }, + { + role: 'user', + content: prompt, + }, + ], + model: 'llama-3.1-70b-versatile', + //model: 'llama-3.1-8b-instant', + temperature: 0.2, + max_tokens: 300, + response_format: { type: 'json_object' }, + }); + + const generatedCriteria = JSON.parse(response.choices[0]?.message?.content); + return generatedCriteria; + + // return response.choices[0]?.message?.content; + } catch (error) { + console.error(`Error calling GPT-4 API: ${response.choices[0]}`, error); + throw new Error('Failed to generate business criteria'); + } + } +} diff --git a/bizmatch-server/src/app.module.ts b/bizmatch-server/src/app.module.ts index 054cf48..4484c2e 100644 --- a/bizmatch-server/src/app.module.ts +++ b/bizmatch-server/src/app.module.ts @@ -5,6 +5,7 @@ import * as dotenv from 'dotenv'; import fs from 'fs-extra'; import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston'; import * as winston from 'winston'; +import { AiModule } from './ai/ai.module.js'; import { AppController } from './app.controller.js'; import { AppService } from './app.service.js'; import { AuthModule } from './auth/auth.module.js'; @@ -73,6 +74,7 @@ loadEnvFiles(); SelectOptionsModule, ImageModule, PassportModule, + AiModule, ], controllers: [AppController], providers: [AppService, FileService], diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index 3b3d974..972a7a3 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -62,6 +62,7 @@ export interface ListCriteria { city: string; prompt: string; searchType: 'exact' | 'radius'; + // radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'; radius: number; criteriaType: 'business' | 'commercialProperty' | 'broker'; } diff --git a/bizmatch-server/src/select-options/select-options.controller.ts b/bizmatch-server/src/select-options/select-options.controller.ts index cd15626..98c58ca 100644 --- a/bizmatch-server/src/select-options/select-options.controller.ts +++ b/bizmatch-server/src/select-options/select-options.controller.ts @@ -14,6 +14,7 @@ export class SelectOptionsController { locations: this.selectOptionsService.locations, typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty, customerSubTypes: this.selectOptionsService.customerSubTypes, + distances: this.selectOptionsService.distances, }; } } diff --git a/bizmatch-server/src/select-options/select-options.service.ts b/bizmatch-server/src/select-options/select-options.service.ts index d79da62..c265c23 100644 --- a/bizmatch-server/src/select-options/select-options.service.ts +++ b/bizmatch-server/src/select-options/select-options.service.ts @@ -35,6 +35,17 @@ export class SelectOptionsService { { name: '$1M', value: '1000000' }, { name: '$5M', value: '5000000' }, ]; + + public distances: Array = [ + { name: '5 miles', value: '5' }, + { name: '20 miles', value: '20' }, + { name: '50 miles', value: '50' }, + { name: '100 miles', value: '100' }, + { name: '200 miles', value: '200' }, + { name: '300 miles', value: '300' }, + { name: '400 miles', value: '400' }, + { name: '500 miles', value: '500' }, + ]; public listingCategories: Array = [ { name: 'Business', value: 'business' }, { name: 'Commercial Property', value: 'commercialProperty' }, diff --git a/bizmatch/src/app/app.component.html b/bizmatch/src/app/app.component.html index 92f37ba..7832567 100644 --- a/bizmatch/src/app/app.component.html +++ b/bizmatch/src/app/app.component.html @@ -7,28 +7,13 @@ - - -@if (loadingService.isLoading$ | async) { +
{{ loadingText }}
-} +} --> diff --git a/bizmatch/src/app/app.routes.ts b/bizmatch/src/app/app.routes.ts index ba318e4..6ea8c15 100644 --- a/bizmatch/src/app/app.routes.ts +++ b/bizmatch/src/app/app.routes.ts @@ -8,6 +8,7 @@ import { DetailsBusinessListingComponent } from './pages/details/details-busines import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component'; import { DetailsUserComponent } from './pages/details/details-user/details-user.component'; import { HomeComponent } from './pages/home/home.component'; +import { Home1Component } from './pages/home1/home1.component'; import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component'; import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component'; import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component'; @@ -39,6 +40,10 @@ export const routes: Routes = [ path: 'home', component: HomeComponent, }, + { + path: 'home1', + component: Home1Component, + }, // ######### // Listings Details { diff --git a/bizmatch/src/app/components/search-modal/search-modal.component.scss b/bizmatch/src/app/components/search-modal/search-modal.component.scss index ad9122b..79b09a7 100644 --- a/bizmatch/src/app/components/search-modal/search-modal.component.scss +++ b/bizmatch/src/app/components/search-modal/search-modal.component.scss @@ -1,4 +1,4 @@ -::ng-deep .ng-select.custom .ng-select-container { +:host ::ng-deep .ng-select.custom .ng-select-container { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); height: 46px; diff --git a/bizmatch/src/app/pages/home/home.component.html b/bizmatch/src/app/pages/home/home.component.html index cd7c470..4bfe2a8 100644 --- a/bizmatch/src/app/pages/home/home.component.html +++ b/bizmatch/src/app/pages/home/home.component.html @@ -36,7 +36,7 @@

Find businesses for sale.

Unlocking Exclusive Opportunities - Empowering Entrepreneurial Dreams

diff --git a/bizmatch/src/app/pages/home/home.component.scss b/bizmatch/src/app/pages/home/home.component.scss index a89757e..0f99c9e 100644 --- a/bizmatch/src/app/pages/home/home.component.scss +++ b/bizmatch/src/app/pages/home/home.component.scss @@ -1,25 +1,3 @@ -// :host { -// height: 100%; -// } - -// .container { -// background-image: url(../../../assets/images/index-bg.webp); -// background-size: cover; -// background-position: center; -// height: 100vh; -// } -// .combo_lp { -// width: 200px; -// } -// .p-button-white { -// color: aliceblue; -// } -// .mt-11 { -// margin-top: 5.9rem !important; -// } -// .mt-22 { -// margin-top: 9.7rem !important; -// } .bg-cover-custom { background-image: url('/assets/images/index-bg.webp'); background-size: cover; @@ -28,3 +6,65 @@ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3); min-height: calc(100vh - 4rem); } +select:not([size]) { + background-image: unset; +} +[type='text'], +[type='email'], +[type='url'], +[type='password'], +[type='number'], +[type='date'], +[type='datetime-local'], +[type='month'], +[type='search'], +[type='tel'], +[type='time'], +[type='week'], +[multiple], +textarea, +select { + border: unset; +} +.toggle-checkbox:checked { + right: 0; + border-color: #4fd1c5; +} +.toggle-checkbox:checked + .toggle-label { + background-color: #4fd1c5; +} +:host ::ng-deep .ng-select.ng-select-single .ng-select-container { + height: 48px; + border: unset; + .ng-value-container .ng-input { + top: 10px; + } + span.ng-arrow-wrapper { + display: none; + } +} +.flex-1-1-2 { + flex: 1 1 2%; +} +// .light { +// color: #999; +// } +// component.css +select { + color: #000; /* Standard-Textfarbe für das Dropdown */ + // background-color: #fff; /* Hintergrundfarbe für das Dropdown */ +} + +select option { + color: #000; /* Textfarbe für Dropdown-Optionen */ +} + +select.placeholder-selected { + color: #999; /* Farbe für den Platzhalter */ +} + +/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */ +select:focus option, +select:hover option { + color: #000 !important; +} diff --git a/bizmatch/src/app/pages/home/home.component.ts b/bizmatch/src/app/pages/home/home.component.ts index 47bee9f..7578826 100644 --- a/bizmatch/src/app/pages/home/home.component.ts +++ b/bizmatch/src/app/pages/home/home.component.ts @@ -1,12 +1,15 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { ChangeDetectorRef, Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { NgSelectModule } from '@ng-select/ng-select'; import { KeycloakService } from 'keycloak-angular'; import onChange from 'on-change'; -import { BusinessListingCriteria, CommercialPropertyListingCriteria, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; +import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs'; +import { BusinessListingCriteria, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { ModalService } from '../../components/search-modal/modal.service'; import { CriteriaChangeService } from '../../services/criteria-change.service'; +import { GeoService } from '../../services/geo.service'; import { ListingsService } from '../../services/listings.service'; import { SearchService } from '../../services/search.service'; import { SelectOptionsService } from '../../services/select-options.service'; @@ -14,7 +17,7 @@ import { getCriteriaStateObject, map2User } from '../../utils/utils'; @Component({ selector: 'app-home', standalone: true, - imports: [CommonModule, FormsModule, RouterModule], + imports: [CommonModule, FormsModule, RouterModule, NgSelectModule], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) @@ -28,6 +31,10 @@ export class HomeComponent { isMenuOpen = false; user: KeycloakUser; prompt: string; + cities$: Observable; + cityLoading = false; + cityInput$ = new Subject(); + cityOrState = undefined; public constructor( private router: Router, private modalService: ModalService, @@ -37,6 +44,8 @@ export class HomeComponent { public keycloakService: KeycloakService, private listingsService: ListingsService, private criteriaChangeService: CriteriaChangeService, + private geoService: GeoService, + public cdRef: ChangeDetectorRef, ) {} async ngOnInit() { const token = await this.keycloakService.getToken(); @@ -45,6 +54,7 @@ export class HomeComponent { sessionStorage.removeItem('broker_criteria'); this.criteria = this.createEnhancedProxy(getCriteriaStateObject('business')); this.user = map2User(token); + this.loadCities(); } async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') { this.activeTabAction = tabname; @@ -90,6 +100,22 @@ export class HomeComponent { toggleMenu() { this.isMenuOpen = !this.isMenuOpen; } + onTypesChange(value) { + if (value === '') { + // Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array + this.criteria.types = []; + } else { + this.criteria.types = [value]; + } + } + onRadiusChange(value) { + if (value === 'null') { + // Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array + this.criteria.radius = null; + } else { + this.criteria.radius = value; + } + } async openModal() { const accepted = await this.modalService.showModal(this.criteria); if (accepted) { @@ -97,4 +123,33 @@ export class HomeComponent { this.router.navigate([`${this.activeTabAction}Listings`]); } } + private loadCities() { + this.cities$ = concat( + of([]), // default items + this.cityInput$.pipe( + distinctUntilChanged(), + tap(() => (this.cityLoading = true)), + switchMap(term => + this.geoService.findCitiesStartingWith(term).pipe( + catchError(() => of([])), // empty list on error + // map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names + tap(() => (this.cityLoading = false)), + ), + ), + ), + ); + } + trackByFn(item: GeoResult) { + return item.id; + } + setCity(city) { + if (city) { + this.criteria.city = city.city; + this.criteria.state = city.state_code; + } else { + this.criteria.city = null; + this.criteria.radius = null; + this.criteria.searchType = 'exact'; + } + } } diff --git a/bizmatch/src/app/services/select-options.service.ts b/bizmatch/src/app/services/select-options.service.ts index 720c54e..18cf5a2 100644 --- a/bizmatch/src/app/services/select-options.service.ts +++ b/bizmatch/src/app/services/select-options.service.ts @@ -21,6 +21,7 @@ export class SelectOptionsService { this.states = allSelectOptions.locations; this.gender = allSelectOptions.gender; this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty; + this.distances = allSelectOptions.distances; } public typesOfBusiness: Array; @@ -36,6 +37,7 @@ export class SelectOptionsService { public states: Array; public customerSubTypes: Array; + public distances: Array; getState(value: string): string { return this.states.find(l => l.value === value)?.name; }