Spaces:
Sleeping
Sleeping
Upload 27 files
Browse files- .env +9 -0
- .env.example +3 -0
- .gitignore +1 -0
- Dockerfile +13 -0
- main.py +3 -0
- project/__init__.py +27 -0
- project/__pycache__/__init__.cpython-310.pyc +0 -0
- project/__pycache__/config.cpython-310.pyc +0 -0
- project/asgi.py +4 -0
- project/bot/__init__.py +7 -0
- project/bot/__pycache__/__init__.cpython-310.pyc +0 -0
- project/bot/__pycache__/chatbot.cpython-310.pyc +0 -0
- project/bot/__pycache__/views.cpython-310.pyc +0 -0
- project/bot/chatbot.py +156 -0
- project/bot/search_tools/cleaned_products.csv +34 -0
- project/bot/search_tools/products_faiss.index +0 -0
- project/bot/templates/voice.html +51 -0
- project/bot/utils.py +108 -0
- project/bot/views.py +11 -0
- project/config.py +66 -0
- project/ws/__init__.py +5 -0
- project/ws/__pycache__/__init__.cpython-310.pyc +0 -0
- project/ws/__pycache__/views.cpython-310.pyc +0 -0
- project/ws/views.py +16 -0
- requirements.txt +37 -0
- static/css/style.css +543 -0
- static/js/voice.js +146 -0
.env
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FASTAPI_CONFIG=production
|
2 |
+
SECRET='zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#'
|
3 |
+
OPENAI_API_KEY=sk-proj-GYY7hx5SM02oDz5lhrK0T3BlbkFJegeuu9bXdNONd3Ds1rqY
|
4 |
+
|
5 |
+
#DATABASE_USER=
|
6 |
+
#DATABASE_PASSWORD=
|
7 |
+
#DATABASE_HOST=
|
8 |
+
#DATABASE_PORT=
|
9 |
+
#DATABASE_NAME=
|
.env.example
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
FASTAPI_CONFIG=production
|
2 |
+
SECRET='zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#'
|
3 |
+
OPENAI_API_KEY=
|
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
.env
|
Dockerfile
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
|
7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
RUN chmod -R 777 /code
|
12 |
+
|
13 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
main.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from project import create_app
|
2 |
+
|
3 |
+
app = create_app()
|
project/__init__.py
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from fastapi.staticfiles import StaticFiles
|
4 |
+
|
5 |
+
from project.config import settings
|
6 |
+
|
7 |
+
|
8 |
+
def create_app() -> FastAPI:
|
9 |
+
app = FastAPI()
|
10 |
+
|
11 |
+
from project.bot import bot_router
|
12 |
+
app.include_router(bot_router, tags=['bot'])
|
13 |
+
|
14 |
+
from project.ws import ws_router
|
15 |
+
app.include_router(ws_router, tags=['ws'])
|
16 |
+
|
17 |
+
app.add_middleware(
|
18 |
+
CORSMiddleware,
|
19 |
+
allow_origins=settings.ORIGINS,
|
20 |
+
allow_credentials=True,
|
21 |
+
allow_methods=["*"],
|
22 |
+
allow_headers=["*"],
|
23 |
+
)
|
24 |
+
|
25 |
+
app.mount('/static', StaticFiles(directory="static"), name="static")
|
26 |
+
|
27 |
+
return app
|
project/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (879 Bytes). View file
|
|
project/__pycache__/config.cpython-310.pyc
ADDED
Binary file (4 kB). View file
|
|
project/asgi.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from project import create_app
|
2 |
+
|
3 |
+
app = create_app()
|
4 |
+
|
project/bot/__init__.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter
|
2 |
+
|
3 |
+
bot_router = APIRouter(
|
4 |
+
prefix=''
|
5 |
+
)
|
6 |
+
|
7 |
+
from project.bot import views
|
project/bot/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (278 Bytes). View file
|
|
project/bot/__pycache__/chatbot.cpython-310.pyc
ADDED
Binary file (4.58 kB). View file
|
|
project/bot/__pycache__/views.cpython-310.pyc
ADDED
Binary file (565 Bytes). View file
|
|
project/bot/chatbot.py
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import base64
|
3 |
+
import os
|
4 |
+
import tempfile
|
5 |
+
|
6 |
+
import numpy as np
|
7 |
+
|
8 |
+
from project.config import settings
|
9 |
+
import pandas as pd
|
10 |
+
|
11 |
+
|
12 |
+
class ChatBot:
|
13 |
+
chat_history = []
|
14 |
+
|
15 |
+
def __init__(self, memory=None):
|
16 |
+
self.chat_history.append({
|
17 |
+
"role": 'assistant',
|
18 |
+
'content': "Hi! What would you like to order from the food?"
|
19 |
+
})
|
20 |
+
|
21 |
+
@staticmethod
|
22 |
+
def _transform_bytes_to_file(data_bytes) -> str:
|
23 |
+
audio_bytes = base64.b64decode(data_bytes)
|
24 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
|
25 |
+
try:
|
26 |
+
temp_file.write(audio_bytes)
|
27 |
+
filepath = temp_file.name
|
28 |
+
finally:
|
29 |
+
temp_file.close()
|
30 |
+
return filepath
|
31 |
+
|
32 |
+
@staticmethod
|
33 |
+
async def _transcript_audio(temp_filepath: str) -> str:
|
34 |
+
with open(temp_filepath, 'rb') as file:
|
35 |
+
transcript = await settings.OPENAI_CLIENT.audio.transcriptions.create(
|
36 |
+
model='whisper-1',
|
37 |
+
file=file,
|
38 |
+
# language='nl'
|
39 |
+
)
|
40 |
+
text = transcript.text
|
41 |
+
return text
|
42 |
+
|
43 |
+
@staticmethod
|
44 |
+
async def _convert_to_embeddings(query: str):
|
45 |
+
response = await settings.OPENAI_CLIENT.embeddings.create(
|
46 |
+
input=query,
|
47 |
+
model='text-embedding-3-large'
|
48 |
+
)
|
49 |
+
embeddings = response.data[0].embedding
|
50 |
+
return embeddings
|
51 |
+
|
52 |
+
@staticmethod
|
53 |
+
async def _convert_response_to_voice(ai_response: str) -> str:
|
54 |
+
audio = await settings.OPENAI_CLIENT.audio.speech.create(
|
55 |
+
model="tts-1",
|
56 |
+
voice="alloy",
|
57 |
+
input=ai_response
|
58 |
+
)
|
59 |
+
encoded_audio = base64.b64encode(audio.content).decode('utf-8')
|
60 |
+
return encoded_audio
|
61 |
+
|
62 |
+
@staticmethod
|
63 |
+
async def _get_context_data(query: list[float]) -> str:
|
64 |
+
query = np.array([query]).astype('float32')
|
65 |
+
_, distances, indices = settings.FAISS_INDEX.range_search(query.astype('float32'), settings.SEARCH_RADIUS)
|
66 |
+
indices_distances_df = pd.DataFrame({'index': indices, 'distance': distances})
|
67 |
+
filtered_data_df = settings.products_dataset.iloc[indices]
|
68 |
+
filtered_data_df['distance'] = indices_distances_df['distance'].values
|
69 |
+
sorted_data_df: pd.DataFrame = filtered_data_df.sort_values(by='distance').reset_index(drop=True)
|
70 |
+
sorted_data_df = sorted_data_df.drop('distance', axis=1)
|
71 |
+
data = sorted_data_df.head(1).to_dict(orient='records')
|
72 |
+
context_str = ''
|
73 |
+
for row in data:
|
74 |
+
context_str += f'{row["Search"]}\n\n'
|
75 |
+
return context_str
|
76 |
+
|
77 |
+
async def _rag(self, query: str, query_type: str, context: str = None):
|
78 |
+
if context:
|
79 |
+
self.chat_history.append({'role': 'assistant', 'content': context})
|
80 |
+
prompt = settings.PRODUCT_PROMPT
|
81 |
+
else:
|
82 |
+
if 'search' in query_type.lower():
|
83 |
+
prompt = settings.EMPTY_PRODUCT_PROMPT
|
84 |
+
elif 'purchase' in query_type.lower():
|
85 |
+
prompt = settings.ADD_TO_CART_PROMPT
|
86 |
+
elif 'product_list' in query_type.lower():
|
87 |
+
prompt = settings.PRODUCT_LIST_PROMPT
|
88 |
+
else:
|
89 |
+
prompt = settings.EMPTY_PRODUCT_PROMPT
|
90 |
+
self.chat_history.append({
|
91 |
+
'role': 'user',
|
92 |
+
'content': query
|
93 |
+
})
|
94 |
+
messages = [
|
95 |
+
{
|
96 |
+
'role': 'system',
|
97 |
+
'content': f"{prompt}"
|
98 |
+
},
|
99 |
+
]
|
100 |
+
messages += self.chat_history
|
101 |
+
completion = await settings.OPENAI_CLIENT.chat.completions.create(
|
102 |
+
messages=messages,
|
103 |
+
temperature=0,
|
104 |
+
n=1,
|
105 |
+
model="gpt-3.5-turbo",
|
106 |
+
)
|
107 |
+
response = completion.choices[0].message.content
|
108 |
+
self.chat_history.append({'role': 'assistant', 'content': response})
|
109 |
+
return response
|
110 |
+
|
111 |
+
async def _get_query_type(self, query: str) -> str:
|
112 |
+
assistant_message = self.chat_history[-1]['content']
|
113 |
+
messages = [
|
114 |
+
{
|
115 |
+
"role": 'system',
|
116 |
+
'content': settings.ANALYZER_PROMPT
|
117 |
+
},
|
118 |
+
{
|
119 |
+
"role": 'user',
|
120 |
+
"content": f"Assistant message: {assistant_message}\n"
|
121 |
+
f"User response: {query}"
|
122 |
+
}
|
123 |
+
]
|
124 |
+
completion = await settings.OPENAI_CLIENT.chat.completions.create(
|
125 |
+
messages=messages,
|
126 |
+
temperature=0,
|
127 |
+
n=1,
|
128 |
+
model="gpt-3.5-turbo",
|
129 |
+
)
|
130 |
+
response = completion.choices[0].message.content
|
131 |
+
return response
|
132 |
+
|
133 |
+
async def ask(self, data: dict):
|
134 |
+
audio = data['audio']
|
135 |
+
temp_filepath = self._transform_bytes_to_file(audio)
|
136 |
+
transcript = await self._transcript_audio(temp_filepath)
|
137 |
+
query_type = await self._get_query_type(transcript)
|
138 |
+
|
139 |
+
context = None
|
140 |
+
if query_type == 'search':
|
141 |
+
transformed_query = await self._convert_to_embeddings(transcript)
|
142 |
+
context = await self._get_context_data(transformed_query)
|
143 |
+
ai_response = await self._rag(transcript, query_type, context)
|
144 |
+
voice_ai_response = await self._convert_response_to_voice(ai_response)
|
145 |
+
|
146 |
+
data = {
|
147 |
+
'user_query': transcript,
|
148 |
+
'ai_response': ai_response,
|
149 |
+
'voice_response': voice_ai_response
|
150 |
+
}
|
151 |
+
|
152 |
+
try:
|
153 |
+
os.remove(temp_filepath)
|
154 |
+
except FileNotFoundError:
|
155 |
+
pass
|
156 |
+
return data
|
project/bot/search_tools/cleaned_products.csv
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Search
|
2 |
+
Naam van de producten: Maak je eigen Poke . Productomschrijving: nan. Prijs: € 14.95
|
3 |
+
"Naam van de producten: So Salmon Bowl. Productomschrijving: Gemarineerde zalm, komkommer, avocado, wortel, edamame, sesam zaadjes, gebakken ui en Japanse mayo. Prijs: € 14.95"
|
4 |
+
"Naam van de producten: Rock 'n chicken. Productomschrijving: Krokante kip, mais, komkommer, edamame bonen, rode ui, gebakken ui, chilisaus dressing, sesamzaadjes.. Prijs: € 14.95"
|
5 |
+
"Naam van de producten: So Salmon Bowl. Productomschrijving: Gemarineerde zalm, komkommer, avocado, wortel, edamame, sesam zaadjes, gebakken ui en Japanse mayo. Prijs: € 14.95"
|
6 |
+
"Naam van de producten: Truly Tuna. Productomschrijving: Raw Tuna, avacado, wortel, komkommer, mais, gebakken ui, nori flakes, wasabi malunaise. Prijs: € 14.95"
|
7 |
+
"Naam van de producten: Rock 'n chicken. Productomschrijving: Krokante kip, mais, komkommer, edamame bonen, rode ui, gebakken ui, chilisaus dressing, sesamzaadjes.. Prijs: € 14.95"
|
8 |
+
"Naam van de producten: I'm Veggie Baby. Productomschrijving: Knapperige tofu, Komkommer, ananas, lente ui, mango, sriracha saus, kokos vlokken en sesamzaadjes.. Prijs: € 14.5"
|
9 |
+
"Naam van de producten: Teriyaki Chicks. Productomschrijving: Krokante kip, komkommer, mais, edamame, wortel, sesamzaadjes, gebakken ui, teriyaki dressing. Prijs: € 14.95"
|
10 |
+
"Naam van de producten: Spicy Tuna. Productomschrijving: Spicy gemarineerde tonijn, Edamame, mango, wakame, mais, masago en gebakken ui.. Prijs: € 14.95"
|
11 |
+
"Naam van de producten: Tempura Trouble. Productomschrijving: Ebi tempura garnalen, Avocado, edamame, wortel, komkommer, furikake en gebakken uitjes. Sriracha malunaise.. Prijs: € 14.95"
|
12 |
+
Naam van de producten: Maak je eigen Poke . Productomschrijving: nan. Prijs: € 14.95
|
13 |
+
Naam van de producten: Edamame. Productomschrijving: Japanse sojaboontjes. Deze zijn op smaak gebracht met een beetje zeezout. Prijs: € 5.95
|
14 |
+
"Naam van de producten: Chuka Wakame. Productomschrijving: Zeewier salade op smaak met sesamolie, sojasaus en sesamzaadjes. Prijs: € 5.95"
|
15 |
+
Naam van de producten: Ebi Fry - 4 Stuks. Productomschrijving: Gefrituurde garnaal.. Prijs: € 5.95
|
16 |
+
Naam van de producten: Karaage - 5 Stuks. Productomschrijving: Gefrituurde kipstukjes in Japanese style. Prijs: € 2.98
|
17 |
+
"Naam van de producten: Malumpia. Productomschrijving: Malu´s treasure, 8 mini loempia's.. Prijs: € 5.95"
|
18 |
+
Naam van de producten: Gyoza. Productomschrijving: Japanse Dumpling. 5 stuks.. Prijs: € 5.95
|
19 |
+
Naam van de producten: Never Enough. Productomschrijving: Malu's zeldzame kipstukken geserveerd met een overheerlijke chilisaus. 5 stuks.. Prijs: € 5.95
|
20 |
+
"Naam van de producten: Pink Lady. Productomschrijving: Aardbei, banaan, soja-vanillemelk. Prijs: € 4.5"
|
21 |
+
"Naam van de producten: Sweet Baby. Productomschrijving: Framboos, soja-vanillemelk, banaan. Prijs: € 4.5"
|
22 |
+
"Naam van de producten: Power Shake. Productomschrijving: Framboos, aarbei, soja-vanillemelk. Prijs: € 4.5"
|
23 |
+
"Naam van de producten: Aloha Shake. Productomschrijving: Avocado, banaan, soja-vanillemelk. Prijs: € 4.5"
|
24 |
+
"Naam van de producten: Choco Loco. Productomschrijving: Banaan, avocado, soja-chocolademelk. Prijs: € 4.5"
|
25 |
+
"Naam van de producten: Coca Cola original taste 330ml. Productomschrijving: Coca-Cola Original Taste, een heerlijke en verfrissende bruisende frisdrank, voor het eerst ontwikkeld in 1886, met een geweldige smaak. Made for sharing. Prijs: € 2.5"
|
26 |
+
Naam van de producten: Coca-Cola Light 330ml. Productomschrijving: Coca-Cola Light is een heerlijke calorie- en suikervrije bruisende frisdrank met een geweldige smaak en slechts 1 kcal per 330ml. Iconische en verfrissende cola. Prijs: € 2.5
|
27 |
+
"Naam van de producten: Fanta Orange 330ml. Productomschrijving: Fanta Orange: de iconische, heerlijke bruisende frisdrank gemaakt met vruchtensap. Perfect voor snacken en ontwikkeld om kleur aan je leven toe te voegen. Prijs: € 2.5"
|
28 |
+
Naam van de producten: Hawaii Tropical Drink. Productomschrijving: nan. Prijs: € 2.5
|
29 |
+
"Naam van de producten: Sprite zero sugar 330ml. Productomschrijving: Laat je ware ik sprankelen met Sprite: een zuivere, fris smakende, bruisende limoen- en citroenfrisdrank. 100% natuurlijke aroma's, zonder suiker, zonder calorieën.. Prijs: € 2.5"
|
30 |
+
"Naam van de producten: Fuze Tea black tea sparkling lemon 330ml. Productomschrijving: FUZETEA Black Tea Sparkling Lemon is een unieke, caloriearme* bruisende theedrank gemaakt met een fusie van zwarte thee en zonnig citroen.. Prijs: € 2.5"
|
31 |
+
"Naam van de producten: Fuze Tea green tea 330ml. Productomschrijving: FUZETEA Green Tea Citrus is een unieke, verfrissende caloriearme* theedrank gemaakt met een fusie van groene thee en citrus.. Prijs: € 2.5"
|
32 |
+
Naam van de producten: Red Dragon. Productomschrijving: Framboos - Banaan en Appelsap. Prijs: € 4.5
|
33 |
+
Naam van de producten: Tropical Island. Productomschrijving: Passievrucht - Ananas - Appelsap. Prijs: € 4.5
|
34 |
+
Naam van de producten: Spicy Dutch. Productomschrijving: Wortel - Appelsap - Gember. Prijs: € 4.5
|
project/bot/search_tools/products_faiss.index
ADDED
Binary file (406 kB). View file
|
|
project/bot/templates/voice.html
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6 |
+
<title>Real Estate</title>
|
7 |
+
<!-- Fonts -->
|
8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
10 |
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500&display=swap" rel="stylesheet">
|
11 |
+
<!-- Bootstrap css -->
|
12 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
|
13 |
+
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
14 |
+
<link rel="stylesheet" href="../../../static/css/style.css">
|
15 |
+
</head>
|
16 |
+
|
17 |
+
<div id="message"></div>
|
18 |
+
|
19 |
+
<div class="overlay" id="loadingModal">
|
20 |
+
<div class="report-loader">
|
21 |
+
<div class="report-inner report-one"></div>
|
22 |
+
<div class="report-inner report-two"></div>
|
23 |
+
<div class="report-inner report-three"></div>
|
24 |
+
</div>
|
25 |
+
</div>
|
26 |
+
<div class="container-fluid px-0" style="height: 100vh">
|
27 |
+
<div class="row mx-0 d-flex align-items-center" style="height: 100vh">
|
28 |
+
<div class="col-4 ms-2">
|
29 |
+
<div class="rounded-5 border shadow" style="height: 95vh ;background-color: #f1f1f1">
|
30 |
+
<div id="chatHistory" class="my-4" style="height: 90vh; overflow-y: auto" >
|
31 |
+
|
32 |
+
</div>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
<div class="col-6 align-items-center justify-content-around d-flex">
|
36 |
+
<button class="btn btn-lg btn-success" onclick="startCall()">Start call</button>
|
37 |
+
<button class="btn btn-lg btn-primary" onclick="stopAndSend()">Send a record</button>
|
38 |
+
<button class="btn btn-lg btn-danger" onclick="endCall()">End call</button>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
</html>
|
43 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
|
44 |
+
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
|
45 |
+
crossorigin="anonymous"></script>
|
46 |
+
<script src="https://code.jquery.com/jquery-3.6.3.min.js"
|
47 |
+
integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
|
48 |
+
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
49 |
+
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
50 |
+
<script src="https://kit.fontawesome.com/d4ffd37f75.js" crossorigin="anonymous"></script>
|
51 |
+
<script type="text/javascript" src="../../../static/js/voice.js"></script>
|
project/bot/utils.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import base64
|
2 |
+
import datetime
|
3 |
+
import os
|
4 |
+
import tempfile
|
5 |
+
from io import BytesIO
|
6 |
+
|
7 |
+
from PIL import Image
|
8 |
+
import aiofiles
|
9 |
+
from fastapi import UploadFile
|
10 |
+
from fastapi.params import File
|
11 |
+
|
12 |
+
from project.bot.models import Report
|
13 |
+
from project.config import settings
|
14 |
+
|
15 |
+
|
16 |
+
async def generate_ai_report(history: list[str], language: str) -> Report:
|
17 |
+
history = list(map(lambda x: {'role': 'user', 'content': x}, history))
|
18 |
+
messages = [
|
19 |
+
{
|
20 |
+
'role': 'system',
|
21 |
+
'content': f"""Summarize the key points from the user's messages, organizing the summary into a structured
|
22 |
+
format. Conclude with a brief report that encapsulates the essence of the
|
23 |
+
discussion. Make your answer in {language}"""
|
24 |
+
},
|
25 |
+
*history
|
26 |
+
]
|
27 |
+
chat_completion = await settings.OPENAI_CLIENT.chat.completions.create(
|
28 |
+
model="gpt-4-0125-preview",
|
29 |
+
temperature=0.4,
|
30 |
+
n=1,
|
31 |
+
messages=messages
|
32 |
+
)
|
33 |
+
response = chat_completion.choices[0].message.content
|
34 |
+
report = Report()
|
35 |
+
report.content = response
|
36 |
+
return report
|
37 |
+
|
38 |
+
|
39 |
+
async def encode_file_to_base64(filepath):
|
40 |
+
async with aiofiles.open(filepath, 'rb') as file:
|
41 |
+
content = await file.read()
|
42 |
+
return base64.b64encode(content).decode('utf-8')
|
43 |
+
|
44 |
+
|
45 |
+
async def transcript_audio_from_base64(data: str):
|
46 |
+
current_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
47 |
+
file_name = f"voice_record_{current_time}.mp3"
|
48 |
+
file_path = str(settings.BASE_DIR / 'project' / 'records' / file_name)
|
49 |
+
data_bytes = base64.b64decode(data)
|
50 |
+
async with aiofiles.open(file_path, 'wb') as f:
|
51 |
+
await f.write(data_bytes)
|
52 |
+
with open(file_path, 'rb') as f:
|
53 |
+
transcript = await settings.OPENAI_CLIENT.audio.transcriptions.create(
|
54 |
+
model='whisper-1',
|
55 |
+
file=f,
|
56 |
+
)
|
57 |
+
text = transcript.text
|
58 |
+
return text, file_path
|
59 |
+
|
60 |
+
|
61 |
+
async def generate_image_description(image: str, file_format: str) -> str:
|
62 |
+
messages = [
|
63 |
+
{
|
64 |
+
'role': 'system',
|
65 |
+
'content': settings.REPORT_PROMPT
|
66 |
+
},
|
67 |
+
{
|
68 |
+
'role': 'user',
|
69 |
+
'content': [
|
70 |
+
{
|
71 |
+
'type': 'text',
|
72 |
+
'text': settings.IMAGE_PROMPT
|
73 |
+
},
|
74 |
+
{
|
75 |
+
'type': 'image_url',
|
76 |
+
'image_url': {
|
77 |
+
'url': f"data:image/{file_format};base64,{image}",
|
78 |
+
'detail': 'low'
|
79 |
+
}
|
80 |
+
}
|
81 |
+
]
|
82 |
+
}
|
83 |
+
]
|
84 |
+
chat_completion = await settings.OPENAI_CLIENT.chat.completions.create(
|
85 |
+
model="gpt-4-vision-preview",
|
86 |
+
temperature=0.5,
|
87 |
+
n=1,
|
88 |
+
messages=messages
|
89 |
+
)
|
90 |
+
response = chat_completion.choices[0].message.content
|
91 |
+
return response
|
92 |
+
|
93 |
+
|
94 |
+
def compress_and_save_image(image_content: bytes, width=768) -> str:
|
95 |
+
img = Image.open(BytesIO(image_content))
|
96 |
+
orig_width, orig_height = img.size
|
97 |
+
new_height = int((orig_height * width) / orig_width)
|
98 |
+
resized_img = img.resize((width, new_height), Image.LANCZOS)
|
99 |
+
current_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
100 |
+
file_format = img.format.lower()
|
101 |
+
file_path = str(settings.BASE_DIR / 'project' / 'images' / f'image_{current_time}.{file_format}')
|
102 |
+
if file_format in ['jpeg', 'jpg']:
|
103 |
+
resized_img.save(file_path, 'JPEG', optimize=True, quality=70)
|
104 |
+
elif file_format == 'png':
|
105 |
+
resized_img.save(file_path, 'PNG', optimize=True, compress_level=7)
|
106 |
+
else:
|
107 |
+
raise ValueError(f"{file_format.upper()} format is not supported.")
|
108 |
+
return file_path
|
project/bot/views.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi.templating import Jinja2Templates
|
2 |
+
from fastapi.requests import Request
|
3 |
+
|
4 |
+
from project.bot import bot_router
|
5 |
+
|
6 |
+
template = Jinja2Templates(directory='project/bot/templates')
|
7 |
+
|
8 |
+
|
9 |
+
@bot_router.get('/', name='voice')
|
10 |
+
async def voice(request: Request):
|
11 |
+
return template.TemplateResponse("voice.html", {'request': request})
|
project/config.py
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pathlib
|
2 |
+
from functools import lru_cache
|
3 |
+
|
4 |
+
import pandas as pd
|
5 |
+
from openai import AsyncOpenAI
|
6 |
+
from environs import Env
|
7 |
+
import faiss
|
8 |
+
|
9 |
+
env = Env()
|
10 |
+
env.read_env()
|
11 |
+
|
12 |
+
|
13 |
+
class BaseConfig:
|
14 |
+
BASE_DIR: pathlib.Path = pathlib.Path(__file__).parent.parent
|
15 |
+
OPENAI_CLIENT = AsyncOpenAI(api_key=env('OPENAI_API_KEY'))
|
16 |
+
FAISS_INDEX = faiss.read_index(str(BASE_DIR / 'project' / 'bot' / 'search_tools' / 'products_faiss.index'))
|
17 |
+
SEARCH_RADIUS = 1
|
18 |
+
PRODUCT_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak is om het product " \
|
19 |
+
"zeer kort en bondig aan de gebruiker te presenteren. De gebruiker zal vragen om voedsel " \
|
20 |
+
"te bestellen in de winkel, en u, met behulp van uw vorige antwoord als kennis van " \
|
21 |
+
"productinformatie, moet kort vertellen over het product, de prijs en aanbieden om het toe "
|
22 |
+
EMPTY_PRODUCT_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak is om de gebruiker heel kort en bondig te vragen om informatie over het voedsel in meer detail te verstrekken."
|
23 |
+
ANALYZER_PROMPT = """
|
24 |
+
Je bent een model voor classificatie. Analyseer de boodschap van de assistent en het antwoord van de gebruiker. Je taak is om hun dialoog te classificeren naar een van de antwoordcategorieën "search", "product_list", "purchase".
|
25 |
+
search - wanneer de gebruiker voedselkenmerken, naam, prijs opsomt. De gebruiker zal vragen om een specifiek product te vinden
|
26 |
+
product_list - wanneer de gebruiker vraagt om alle producten die aan de winkelwagen zijn toegevoegd, weer te geven. Of alle producten die hij van plan is te bestellen.
|
27 |
+
purchase - wanneer de assistent voorstelt om een product aan de winkelwagen toe te voegen en de gebruiker akkoord gaat.
|
28 |
+
Dus je antwoord moet slechts één woord bevatten - de categorie waartoe de dialoog van de assistent en de gebruiker behoort (search, product_list of purchase).
|
29 |
+
"""
|
30 |
+
ADD_TO_CART_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak is om heel kort en bondig te reageren op het verzoek van de gebruiker om voedsel aan het winkelwagentje toe te voegen. Stel bijvoorbeeld dat het gevraagde product aan de winkelwagen is toegevoegd en bied aan om iets anders te kopen"
|
31 |
+
PRODUCT_LIST_PROMPT = "Je bent de virtuele assistent van de Malu Haarlemse winkel. Uw taak, op verzoek van de gebruiker, is om een lijst van alle producten toegevoegd aan de winkelwagen en vertellen de totale kosten. Analyseer de geschiedenis van de dialoog om te begrijpen welke producten de gebruiker aan de winkelwagen heeft toegevoegd"
|
32 |
+
|
33 |
+
|
34 |
+
class DevelopmentConfig(BaseConfig):
|
35 |
+
pass
|
36 |
+
|
37 |
+
|
38 |
+
class ProductionConfig(BaseConfig):
|
39 |
+
ORIGINS = ["http://127.0.0.1:8000",
|
40 |
+
"http://localhost:8000",
|
41 |
+
'https://alcolm.com/',
|
42 |
+
'https://help.alcolm.com/'
|
43 |
+
]
|
44 |
+
|
45 |
+
def __init__(self):
|
46 |
+
self.products_dataset = pd.read_csv(
|
47 |
+
str(self.BASE_DIR / 'project' / 'bot' / 'search_tools' / 'cleaned_products.csv'))
|
48 |
+
|
49 |
+
|
50 |
+
class TestConfig(BaseConfig):
|
51 |
+
pass
|
52 |
+
|
53 |
+
|
54 |
+
@lru_cache()
|
55 |
+
def get_settings() -> DevelopmentConfig | ProductionConfig | TestConfig:
|
56 |
+
config_cls_dict = {
|
57 |
+
'development': DevelopmentConfig,
|
58 |
+
'production': ProductionConfig,
|
59 |
+
'testing': TestConfig
|
60 |
+
}
|
61 |
+
config_name = env('FASTAPI_CONFIG', default='production')
|
62 |
+
config_cls = config_cls_dict[config_name]
|
63 |
+
return config_cls()
|
64 |
+
|
65 |
+
|
66 |
+
settings = get_settings()
|
project/ws/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter
|
2 |
+
|
3 |
+
ws_router = APIRouter()
|
4 |
+
|
5 |
+
from . import views
|
project/ws/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (250 Bytes). View file
|
|
project/ws/__pycache__/views.cpython-310.pyc
ADDED
Binary file (681 Bytes). View file
|
|
project/ws/views.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import WebSocket, WebSocketDisconnect
|
2 |
+
from . import ws_router
|
3 |
+
from ..bot.chatbot import ChatBot
|
4 |
+
|
5 |
+
|
6 |
+
@ws_router.websocket("/ws/{client_id}")
|
7 |
+
async def websocket_endpoint(websocket: WebSocket, client_id: str):
|
8 |
+
await websocket.accept()
|
9 |
+
chatbot = ChatBot()
|
10 |
+
try:
|
11 |
+
while True:
|
12 |
+
data = await websocket.receive_json()
|
13 |
+
answer = await chatbot.ask(data)
|
14 |
+
await websocket.send_json(answer)
|
15 |
+
except WebSocketDisconnect:
|
16 |
+
pass
|
requirements.txt
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
annotated-types==0.6.0
|
2 |
+
anyio==4.3.0
|
3 |
+
certifi==2024.2.2
|
4 |
+
click==8.1.7
|
5 |
+
distro==1.9.0
|
6 |
+
environs==11.0.0
|
7 |
+
exceptiongroup==1.2.1
|
8 |
+
faiss-gpu==1.7.2
|
9 |
+
fastapi==0.110.3
|
10 |
+
h11==0.14.0
|
11 |
+
httpcore==1.0.5
|
12 |
+
httptools==0.6.1
|
13 |
+
httpx==0.27.0
|
14 |
+
idna==3.7
|
15 |
+
Jinja2==3.1.3
|
16 |
+
MarkupSafe==2.1.5
|
17 |
+
marshmallow==3.21.2
|
18 |
+
numpy==1.26.4
|
19 |
+
openai==1.25.1
|
20 |
+
packaging==24.0
|
21 |
+
pandas==2.2.2
|
22 |
+
pydantic==2.7.1
|
23 |
+
pydantic_core==2.18.2
|
24 |
+
python-dateutil==2.9.0.post0
|
25 |
+
python-dotenv==1.0.1
|
26 |
+
pytz==2024.1
|
27 |
+
PyYAML==6.0.1
|
28 |
+
six==1.16.0
|
29 |
+
sniffio==1.3.1
|
30 |
+
starlette==0.37.2
|
31 |
+
tqdm==4.66.4
|
32 |
+
typing_extensions==4.11.0
|
33 |
+
tzdata==2024.1
|
34 |
+
uvicorn==0.29.0
|
35 |
+
uvloop==0.19.0
|
36 |
+
watchfiles==0.21.0
|
37 |
+
websockets==12.0
|
static/css/style.css
ADDED
@@ -0,0 +1,543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
code {
|
2 |
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
3 |
+
monospace;
|
4 |
+
}
|
5 |
+
|
6 |
+
::-webkit-scrollbar {
|
7 |
+
width: 10px;
|
8 |
+
}
|
9 |
+
|
10 |
+
::-webkit-scrollbar-track {
|
11 |
+
border-radius: 10px;
|
12 |
+
background: transparent;
|
13 |
+
}
|
14 |
+
|
15 |
+
::-webkit-scrollbar-thumb {
|
16 |
+
background: #14274E;
|
17 |
+
border-radius: 10px
|
18 |
+
}
|
19 |
+
|
20 |
+
::-webkit-scrollbar-thumb:hover {
|
21 |
+
background: #1d3970;
|
22 |
+
}
|
23 |
+
|
24 |
+
|
25 |
+
:root {
|
26 |
+
margin: 0px;
|
27 |
+
}
|
28 |
+
|
29 |
+
.App {
|
30 |
+
display: none;
|
31 |
+
position: fixed;
|
32 |
+
bottom: 20px;
|
33 |
+
left: 20px;
|
34 |
+
z-index: 81;
|
35 |
+
|
36 |
+
width: 540px;
|
37 |
+
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
38 |
+
border-radius: 10px;
|
39 |
+
}
|
40 |
+
|
41 |
+
.App .head-container {
|
42 |
+
background-color: #14274E;
|
43 |
+
padding: 19px 50px 19px 50px;
|
44 |
+
display: flex;
|
45 |
+
align-items: center;
|
46 |
+
border-radius: 10px 10px 0px 0px;
|
47 |
+
}
|
48 |
+
|
49 |
+
.App .head-container .head-log-wrapper {
|
50 |
+
display: flex;
|
51 |
+
}
|
52 |
+
|
53 |
+
.App .head-container .head-log-wrapper .head-icon {
|
54 |
+
margin-right: 15.72px;
|
55 |
+
}
|
56 |
+
|
57 |
+
.App .head-container .head-log-wrapper .head-content {
|
58 |
+
position: relative;
|
59 |
+
margin-top: 5px;
|
60 |
+
}
|
61 |
+
|
62 |
+
.App .head-container .head-log-wrapper .head-content p {
|
63 |
+
margin: 0px;
|
64 |
+
position: absolute;
|
65 |
+
bottom: -1px;
|
66 |
+
color: white;
|
67 |
+
font-weight: 500;
|
68 |
+
letter-spacing: 0.755px;
|
69 |
+
font-size: 16px;
|
70 |
+
margin-left: 18px;
|
71 |
+
}
|
72 |
+
|
73 |
+
.App .head-container .head-close-wrapper {
|
74 |
+
margin-left: auto;
|
75 |
+
display: flex;
|
76 |
+
cursor: pointer;
|
77 |
+
}
|
78 |
+
|
79 |
+
.App .head-container .head-close-wrapper p {
|
80 |
+
margin: 0px;
|
81 |
+
color: #EF9E00;
|
82 |
+
font-size: 18px;
|
83 |
+
font-style: normal;
|
84 |
+
font-weight: 500;
|
85 |
+
}
|
86 |
+
|
87 |
+
.App .head-container .head-close-wrapper span {
|
88 |
+
display: flex;
|
89 |
+
align-items: center;
|
90 |
+
}
|
91 |
+
|
92 |
+
.App .chatui-container {
|
93 |
+
background-image: url('/static/bgs/bg.png');
|
94 |
+
background-size: cover;
|
95 |
+
background-repeat: no-repeat;
|
96 |
+
|
97 |
+
padding-top: 20px;
|
98 |
+
padding-left: 35px;
|
99 |
+
padding-right: 35px;
|
100 |
+
overflow-y: scroll;
|
101 |
+
/* Define the animation for scrolling down */
|
102 |
+
scroll-behavior: smooth;
|
103 |
+
}
|
104 |
+
|
105 |
+
.App .chatui-container .user-msg {
|
106 |
+
max-width: 360px;
|
107 |
+
margin-left: auto;
|
108 |
+
border: 2px solid #EF9E00;
|
109 |
+
padding: 10px 10px 10px 10px;
|
110 |
+
border-radius: 15px 0px 15px 15px;
|
111 |
+
background: #FCF4E5;
|
112 |
+
word-wrap: break-word;
|
113 |
+
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
114 |
+
margin-bottom: 40px;
|
115 |
+
}
|
116 |
+
|
117 |
+
.App .chatui-container .bot-msg {
|
118 |
+
display: flex;
|
119 |
+
}
|
120 |
+
|
121 |
+
.App .chatui-container .bot-avatar-box span {
|
122 |
+
background-color: #14274E;
|
123 |
+
width: 55px;
|
124 |
+
height: 55px;
|
125 |
+
border-radius: 55px;
|
126 |
+
display: flex;
|
127 |
+
align-items: center;
|
128 |
+
justify-content: center;
|
129 |
+
margin-bottom: 10px;
|
130 |
+
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
131 |
+
}
|
132 |
+
|
133 |
+
.App .chatui-container .bot-msg .bot-content-box {
|
134 |
+
max-width: 360px;
|
135 |
+
/* width: 100%; */
|
136 |
+
margin-left: 20px;
|
137 |
+
padding: 10px 15px 10px 15px;
|
138 |
+
border-radius: 0px 15px 15px 15px;
|
139 |
+
border: 2px solid var(--surface-surface-invert, #14274E);
|
140 |
+
background: #E7E8EC;
|
141 |
+
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
142 |
+
margin-bottom: 40px;
|
143 |
+
}
|
144 |
+
|
145 |
+
.App .chatui-container .bot-msg .bot-content-box p {
|
146 |
+
margin: 0 0 0em;
|
147 |
+
<!-- /* min-height: 48px; */
|
148 |
+
-->
|
149 |
+
}
|
150 |
+
|
151 |
+
.App .search-container {
|
152 |
+
padding: 15px 15px 15px 15px;
|
153 |
+
display: flex;
|
154 |
+
background-color: #ffffff;
|
155 |
+
align-items: center;
|
156 |
+
border-radius: 0px 0px 10px 10px;
|
157 |
+
border-bottom: 1px solid rgba(57, 72, 103, 0.1);
|
158 |
+
border-left: 1px solid rgba(57, 72, 103, 0.1);
|
159 |
+
border-right: 1px solid rgba(57, 72, 103, 0.1);
|
160 |
+
}
|
161 |
+
|
162 |
+
.App .search-container .search-input {
|
163 |
+
width: 100%;
|
164 |
+
padding: 15px 20px 15px 20px;
|
165 |
+
font-size: 16px;
|
166 |
+
border: 2px solid #000308;
|
167 |
+
border-radius: 10px 0px 0px 10px;
|
168 |
+
border-right: none;
|
169 |
+
outline: none;
|
170 |
+
}
|
171 |
+
|
172 |
+
.App .search-container .submit-btn {
|
173 |
+
cursor: pointer;
|
174 |
+
}
|
175 |
+
|
176 |
+
.App .search-container .submit-btn span {
|
177 |
+
display: flex;
|
178 |
+
align-items: center;
|
179 |
+
justify-content: center;
|
180 |
+
background-color: #14274E;
|
181 |
+
padding: 11px;
|
182 |
+
border-radius: 0px 10px 10px 0px;
|
183 |
+
}
|
184 |
+
|
185 |
+
|
186 |
+
.sticky-chat-icon {
|
187 |
+
position: fixed;
|
188 |
+
bottom: 20px;
|
189 |
+
left: 20px;
|
190 |
+
width: 80px;
|
191 |
+
height: 80px;
|
192 |
+
text-align: center;
|
193 |
+
cursor: pointer;
|
194 |
+
z-index: 80;
|
195 |
+
}
|
196 |
+
|
197 |
+
|
198 |
+
.chat-wrapper {
|
199 |
+
height: 80px;
|
200 |
+
width: 80px;
|
201 |
+
background-color: #14274e;
|
202 |
+
border-radius: 50% 50% 50% 0;
|
203 |
+
border: 2px solid #ef9e00;
|
204 |
+
display: flex; /* Включить flex-контейнер */
|
205 |
+
justify-content: center; /* Выравнивание по горизонтали по центру */
|
206 |
+
align-items: center;
|
207 |
+
}
|
208 |
+
|
209 |
+
.bot-avatar {
|
210 |
+
fill: white;
|
211 |
+
width: 40px;
|
212 |
+
height: 40px;
|
213 |
+
}
|
214 |
+
|
215 |
+
.sticky-chat-icon:hover .bot-avatar {
|
216 |
+
fill: #ef9e00; /* Цвет при наведении (оранжевый) */
|
217 |
+
width: 45px; /* Размер при наведении (45x45 пикселей) */
|
218 |
+
height: 45px;
|
219 |
+
}
|
220 |
+
|
221 |
+
|
222 |
+
.ball {
|
223 |
+
background-color: #394867;
|
224 |
+
width: 8px;
|
225 |
+
height: 8px;
|
226 |
+
margin: 2px;
|
227 |
+
border-radius: 100%;
|
228 |
+
display: inline-block;
|
229 |
+
animation: bounce 0.5s ease-in-out infinite;
|
230 |
+
}
|
231 |
+
|
232 |
+
.ball1 {
|
233 |
+
animation-delay: 0.1s;
|
234 |
+
animation-direction: alternate; /* Для движения вверх и вниз */
|
235 |
+
}
|
236 |
+
|
237 |
+
.ball2 {
|
238 |
+
animation-delay: 0.2s;
|
239 |
+
animation-direction: alternate;
|
240 |
+
}
|
241 |
+
|
242 |
+
.ball3 {
|
243 |
+
animation-delay: 0.3s;
|
244 |
+
animation-direction: alternate;
|
245 |
+
}
|
246 |
+
|
247 |
+
@keyframes bounce {
|
248 |
+
0%, 100% {
|
249 |
+
transform: translateY(0);
|
250 |
+
}
|
251 |
+
50% {
|
252 |
+
transform: translateY(-7px); /* Для дерганного движения */
|
253 |
+
}
|
254 |
+
}
|
255 |
+
|
256 |
+
/* new loading state */
|
257 |
+
.loading-text {
|
258 |
+
min-height: 48px;
|
259 |
+
}
|
260 |
+
|
261 |
+
.loading-text p {
|
262 |
+
opacity: 0;
|
263 |
+
}
|
264 |
+
|
265 |
+
@keyframes fade-in-out {
|
266 |
+
0% {
|
267 |
+
opacity: 0;
|
268 |
+
}
|
269 |
+
50% {
|
270 |
+
opacity: 1;
|
271 |
+
}
|
272 |
+
100% {
|
273 |
+
opacity: 0;
|
274 |
+
}
|
275 |
+
}
|
276 |
+
|
277 |
+
@keyframes fade-in-out-2 {
|
278 |
+
0% {
|
279 |
+
opacity: 0;
|
280 |
+
}
|
281 |
+
55% {
|
282 |
+
opacity: 1;
|
283 |
+
}
|
284 |
+
100% {
|
285 |
+
opacity: 0;
|
286 |
+
}
|
287 |
+
}
|
288 |
+
|
289 |
+
/* end of new loading state */
|
290 |
+
|
291 |
+
|
292 |
+
.gpt-send-button {
|
293 |
+
background-color: #14274E;
|
294 |
+
width: 100%;
|
295 |
+
display: flex;
|
296 |
+
justify-content: center;
|
297 |
+
border-radius: 10px;
|
298 |
+
border: 2px solid #14274E;
|
299 |
+
align-items: center;
|
300 |
+
cursor: pointer;
|
301 |
+
}
|
302 |
+
|
303 |
+
.gpt-send-button span {
|
304 |
+
color: #FCFDFF;
|
305 |
+
text-align: center;
|
306 |
+
font-size: 20px;
|
307 |
+
padding: 10px 8px;
|
308 |
+
font-style: normal;
|
309 |
+
font-weight: 700;
|
310 |
+
line-height: normal;
|
311 |
+
letter-spacing: -0.4px;
|
312 |
+
}
|
313 |
+
|
314 |
+
.send-button-blocked {
|
315 |
+
background-color: #14274ed8;
|
316 |
+
border: 2px solid #14274ed8 !important;
|
317 |
+
pointer-events: none;
|
318 |
+
cursor: default;
|
319 |
+
}
|
320 |
+
|
321 |
+
.gpt-form {
|
322 |
+
/* max-width: var(--reading-width, 48em); */
|
323 |
+
margin-right: auto;
|
324 |
+
margin-left: auto;
|
325 |
+
margin-bottom: 40px;
|
326 |
+
}
|
327 |
+
|
328 |
+
.gpt-input {
|
329 |
+
height: 46px;
|
330 |
+
width: 100%;
|
331 |
+
margin: 10px 0;
|
332 |
+
display: flex;
|
333 |
+
box-sizing: border-box;
|
334 |
+
padding: 2px 20px;
|
335 |
+
flex-direction: column;
|
336 |
+
justify-content: center;
|
337 |
+
align-items: flex-start;
|
338 |
+
flex: 1 0 0;
|
339 |
+
border-radius: 10px;
|
340 |
+
border: 2px solid #14274E;
|
341 |
+
background-color: #ffffff;
|
342 |
+
color: rgb(42, 43, 42);
|
343 |
+
font-size: 14px;
|
344 |
+
line-height: 1.2em;
|
345 |
+
}
|
346 |
+
|
347 |
+
.gpt-input::placeholder {
|
348 |
+
color: rgba(42, 43, 42, 0.808);
|
349 |
+
}
|
350 |
+
|
351 |
+
.gpt-input.invalid {
|
352 |
+
border: 2px solid rgb(219, 0, 0);
|
353 |
+
}
|
354 |
+
|
355 |
+
|
356 |
+
.error-message {
|
357 |
+
color: rgb(219, 0, 0);
|
358 |
+
font-size: 14px;
|
359 |
+
margin-top: 5px;
|
360 |
+
display: none;
|
361 |
+
}
|
362 |
+
|
363 |
+
.gpt-textarea {
|
364 |
+
height: auto;
|
365 |
+
resize: vertical;
|
366 |
+
padding-top: 20px;
|
367 |
+
font-size: 14px;
|
368 |
+
}
|
369 |
+
|
370 |
+
.gpt-textarea::placeholder {
|
371 |
+
color: rgba(42, 43, 42, 0.808);
|
372 |
+
}
|
373 |
+
|
374 |
+
.custom_bot_initial_question {
|
375 |
+
width: 100%;
|
376 |
+
display: flex;
|
377 |
+
flex-direction: column;
|
378 |
+
row-gap: 10px;
|
379 |
+
max-width: 335px;
|
380 |
+
margin-left: 20px;
|
381 |
+
margin-top: -2.7rem;
|
382 |
+
margin-bottom: 40px;
|
383 |
+
}
|
384 |
+
|
385 |
+
.custom_bot_initial_question p {
|
386 |
+
padding: 5px 12px;
|
387 |
+
font-size: 12px;
|
388 |
+
background: #fff;
|
389 |
+
border: 2px solid #14274E;
|
390 |
+
border-radius: 50px;
|
391 |
+
margin: 0;
|
392 |
+
transition: 0.3s ease;
|
393 |
+
cursor: pointer;
|
394 |
+
font-style: italic;
|
395 |
+
}
|
396 |
+
|
397 |
+
.custom_bot_initial_question p:hover {
|
398 |
+
background: #14274E;
|
399 |
+
color: #fff;
|
400 |
+
}
|
401 |
+
|
402 |
+
@media (max-width: 767px) {
|
403 |
+
.App {
|
404 |
+
width: 100%;
|
405 |
+
height: 100vh;
|
406 |
+
left: 0;
|
407 |
+
bottom: 0;
|
408 |
+
display: none;
|
409 |
+
position: fixed;
|
410 |
+
z-index: 81;
|
411 |
+
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
412 |
+
border-radius: 10px;
|
413 |
+
/* Дополнительные стили для .app на маленьких экранах */
|
414 |
+
}
|
415 |
+
|
416 |
+
.chat-section {
|
417 |
+
height: 100vh;
|
418 |
+
}
|
419 |
+
|
420 |
+
.App .chatui-container {
|
421 |
+
padding-left: 15px;
|
422 |
+
padding-right: 15px;
|
423 |
+
}
|
424 |
+
|
425 |
+
.App .chatui-container .user-msg {
|
426 |
+
padding: 15px 20px;
|
427 |
+
margin-bottom: 20px;
|
428 |
+
}
|
429 |
+
|
430 |
+
.App .chatui-container .bot-msg .bot-content-box {
|
431 |
+
padding: 5px 20px 5px 20px;
|
432 |
+
margin-bottom: 20px;
|
433 |
+
}
|
434 |
+
|
435 |
+
.custom_bot_initial_question {
|
436 |
+
margin-top: 0;
|
437 |
+
}
|
438 |
+
|
439 |
+
#chatui-container {
|
440 |
+
height: calc(100vh - 95px - 93px);
|
441 |
+
}
|
442 |
+
}
|
443 |
+
|
444 |
+
|
445 |
+
.overlay {
|
446 |
+
position: fixed;
|
447 |
+
top: 0;
|
448 |
+
left: 0;
|
449 |
+
width: 100%;
|
450 |
+
height: 100%;
|
451 |
+
background: rgba(255, 255, 255, 0.7);
|
452 |
+
display: flex;
|
453 |
+
align-items: center;
|
454 |
+
justify-content: center;
|
455 |
+
visibility: hidden;
|
456 |
+
z-index: 999;
|
457 |
+
}
|
458 |
+
|
459 |
+
.overlay h1 {
|
460 |
+
margin: 0;
|
461 |
+
font-size: 2em;
|
462 |
+
}
|
463 |
+
|
464 |
+
|
465 |
+
.report-loader {
|
466 |
+
position: absolute;
|
467 |
+
top: calc(50% - 45px);
|
468 |
+
left: calc(50% - 45px);
|
469 |
+
width: 90px;
|
470 |
+
height: 90px;
|
471 |
+
border-radius: 50%;
|
472 |
+
perspective: 800px;
|
473 |
+
}
|
474 |
+
|
475 |
+
.report-inner {
|
476 |
+
position: absolute;
|
477 |
+
box-sizing: border-box;
|
478 |
+
width: 100%;
|
479 |
+
height: 100%;
|
480 |
+
border-radius: 50%;
|
481 |
+
}
|
482 |
+
|
483 |
+
.report-inner.report-one {
|
484 |
+
left: 0;
|
485 |
+
top: 0;
|
486 |
+
animation: rotate-one 1s linear infinite;
|
487 |
+
border-bottom: 3px solid #d52121;
|
488 |
+
}
|
489 |
+
|
490 |
+
.report-inner.report-two {
|
491 |
+
right: 0%;
|
492 |
+
top: 0%;
|
493 |
+
animation: rotate-two 1s linear infinite;
|
494 |
+
border-right: 3px solid #d52121;
|
495 |
+
}
|
496 |
+
|
497 |
+
.report-inner.report-three {
|
498 |
+
right: 0%;
|
499 |
+
bottom: 0%;
|
500 |
+
animation: rotate-three 1s linear infinite;
|
501 |
+
border-top: 3px solid #d52121;
|
502 |
+
}
|
503 |
+
|
504 |
+
@keyframes rotate-one {
|
505 |
+
0% {
|
506 |
+
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
|
507 |
+
}
|
508 |
+
100% {
|
509 |
+
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
|
510 |
+
}
|
511 |
+
}
|
512 |
+
|
513 |
+
@keyframes rotate-two {
|
514 |
+
0% {
|
515 |
+
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
|
516 |
+
}
|
517 |
+
100% {
|
518 |
+
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
|
519 |
+
}
|
520 |
+
}
|
521 |
+
|
522 |
+
@keyframes rotate-three {
|
523 |
+
0% {
|
524 |
+
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
|
525 |
+
}
|
526 |
+
100% {
|
527 |
+
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
|
528 |
+
}
|
529 |
+
}
|
530 |
+
|
531 |
+
#message {
|
532 |
+
display: none;
|
533 |
+
position: absolute;
|
534 |
+
top: 0;
|
535 |
+
right: 0;
|
536 |
+
background-color: #b3ff00;
|
537 |
+
padding: 10px;
|
538 |
+
z-index: 100;
|
539 |
+
}
|
540 |
+
|
541 |
+
.message h5{
|
542 |
+
margin-bottom: 0;
|
543 |
+
}
|
static/js/voice.js
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const initialMessage = ''
|
2 |
+
|
3 |
+
const loadingModal = document.getElementById('loadingModal');
|
4 |
+
const messageHistory = document.getElementById('chatHistory')
|
5 |
+
let mediaRecorder;
|
6 |
+
let audioChunks = [];
|
7 |
+
let socket
|
8 |
+
|
9 |
+
function makeLoading() {
|
10 |
+
loadingModal.style.visibility = 'visible';
|
11 |
+
}
|
12 |
+
|
13 |
+
function stopLoading() {
|
14 |
+
loadingModal.style.visibility = 'hidden';
|
15 |
+
}
|
16 |
+
|
17 |
+
function startCall() {
|
18 |
+
const uuid = generateUUID()
|
19 |
+
messageHistory.innerHTML = ``
|
20 |
+
socket = new WebSocket(`ws://127.0.0.1:8000/ws/${uuid}`);
|
21 |
+
socket.onopen = () => {
|
22 |
+
startRecording()
|
23 |
+
};
|
24 |
+
|
25 |
+
socket.onclose = (event) => console.log('WebSocket disconnected', event);
|
26 |
+
socket.onerror = (error) => {
|
27 |
+
alert('Something was wrong. Try again later.')
|
28 |
+
window.location.reload()
|
29 |
+
};
|
30 |
+
socket.onmessage = (event) => playResponse(event.data);
|
31 |
+
}
|
32 |
+
|
33 |
+
const startRecording = () => {
|
34 |
+
showMessage('You can speak!')
|
35 |
+
navigator.mediaDevices.getUserMedia({audio: true})
|
36 |
+
.then(stream => {
|
37 |
+
mediaRecorder = new MediaRecorder(stream);
|
38 |
+
try {
|
39 |
+
mediaRecorder.ondataavailable = (event) => {
|
40 |
+
audioChunks.push(event.data)
|
41 |
+
|
42 |
+
};
|
43 |
+
} catch (e) {
|
44 |
+
alert('It is not possible to send an empty message')
|
45 |
+
}
|
46 |
+
mediaRecorder.start();
|
47 |
+
})
|
48 |
+
.catch(error => {
|
49 |
+
console.error('Error accessing the microphone', error);
|
50 |
+
});
|
51 |
+
};
|
52 |
+
|
53 |
+
const stopAndSend = () => {
|
54 |
+
makeLoading()
|
55 |
+
mediaRecorder.ondataavailable = (event) => {
|
56 |
+
audioChunks.push(event.data)
|
57 |
+
console.log(audioChunks)
|
58 |
+
|
59 |
+
const audioBlob = new Blob(audioChunks, {type: 'audio/wav'});
|
60 |
+
const reader = new FileReader();
|
61 |
+
reader.readAsDataURL(audioBlob);
|
62 |
+
reader.onloadend = () => {
|
63 |
+
let base64String = reader.result;
|
64 |
+
base64String = base64String.split(',')[1];
|
65 |
+
const dataWS = {
|
66 |
+
'audio': base64String,
|
67 |
+
}
|
68 |
+
console.log(dataWS)
|
69 |
+
socket.send(JSON.stringify(dataWS));
|
70 |
+
audioChunks = [];
|
71 |
+
}
|
72 |
+
}
|
73 |
+
try {
|
74 |
+
mediaRecorder.stop();
|
75 |
+
} catch (e) {
|
76 |
+
return
|
77 |
+
}
|
78 |
+
}
|
79 |
+
|
80 |
+
|
81 |
+
const playResponse = (data, initMessage = false) => {
|
82 |
+
data = JSON.parse(data)
|
83 |
+
if (!initMessage) {
|
84 |
+
const aiResponse = data['ai_response']
|
85 |
+
if (aiResponse.startsWith('https://blue-estate-agency.com/') || aiResponse.startsWith('http://blue-estate-agency.com/')) {
|
86 |
+
window.open(aiResponse, '_blank');
|
87 |
+
}
|
88 |
+
}
|
89 |
+
const audioSrc = `data:audio/mp3;base64,${data['voice_response']}`;
|
90 |
+
const audio = new Audio(audioSrc);
|
91 |
+
stopLoading()
|
92 |
+
if (!initMessage) {
|
93 |
+
createMessage('User', data['user_query'])
|
94 |
+
}
|
95 |
+
audio.play();
|
96 |
+
|
97 |
+
audio.onended = () => {
|
98 |
+
startRecording()
|
99 |
+
if (!initMessage) {
|
100 |
+
createMessage('Assistant', data['ai_response'])
|
101 |
+
}
|
102 |
+
};
|
103 |
+
}
|
104 |
+
|
105 |
+
function endCall() {
|
106 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
107 |
+
mediaRecorder.stop();
|
108 |
+
audioChunks = []
|
109 |
+
}
|
110 |
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
111 |
+
socket.close();
|
112 |
+
}
|
113 |
+
showMessage('The call is ended')
|
114 |
+
}
|
115 |
+
|
116 |
+
function generateUUID() {
|
117 |
+
const arr = new Uint8Array(16);
|
118 |
+
window.crypto.getRandomValues(arr);
|
119 |
+
|
120 |
+
arr[6] = (arr[6] & 0x0f) | 0x40;
|
121 |
+
arr[8] = (arr[8] & 0x3f) | 0x80;
|
122 |
+
|
123 |
+
return ([...arr].map((b, i) =>
|
124 |
+
(i === 4 || i === 6 || i === 8 || i === 10 ? "-" : "") + b.toString(16).padStart(2, "0")
|
125 |
+
).join(""));
|
126 |
+
}
|
127 |
+
|
128 |
+
function showMessage(message_text) {
|
129 |
+
const message = document.getElementById('message');
|
130 |
+
message.innerText = message_text
|
131 |
+
message.style.display = 'block';
|
132 |
+
|
133 |
+
setTimeout(function () {
|
134 |
+
message.style.display = 'none';
|
135 |
+
}, 2000);
|
136 |
+
}
|
137 |
+
|
138 |
+
function createMessage(type, message) {
|
139 |
+
const newMessage = document.createElement('div')
|
140 |
+
newMessage.className = 'message rounded-4 bg-white mb-4 mx-4 py-2 px-3 border'
|
141 |
+
newMessage.innerHTML = `
|
142 |
+
<h5>${type}</h5>
|
143 |
+
${message}
|
144 |
+
`
|
145 |
+
messageHistory.appendChild(newMessage)
|
146 |
+
}
|