brestok commited on
Commit
56779ed
·
verified ·
1 Parent(s): 18ee0e0

Upload 37 files

Browse files
.env CHANGED
@@ -1,7 +1,7 @@
1
  FASTAPI_CONFIG=production
2
  SECRET=zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#
3
- OPENAI_API_KEY=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
4
-
5
  #DATABASE_USER=hectool_ai_filter
6
  #DATABASE_PASSWORD=nixtz3orhepndjw4m1D4
7
  #DATABASE_HOST=localhost
 
1
  FASTAPI_CONFIG=production
2
  SECRET=zu=*nck@&r26rsa$2qv8a7g-p$slw79ym81mylj6s**w(da6&#
3
+ OPENAI_API_KEY=sk-proj-GYY7hx5SM02oDz5lhrK0T3BlbkFJegeuu9bXdNONd3Ds1rqY
4
+ GOOGLE_PLACES_API_KEY=AIzaSyDvND-yu2b3p2FcO5aS7YhuyFI4dsXRBts
5
  #DATABASE_USER=hectool_ai_filter
6
  #DATABASE_PASSWORD=nixtz3orhepndjw4m1D4
7
  #DATABASE_HOST=localhost
project/__pycache__/config.cpython-310.pyc CHANGED
Binary files a/project/__pycache__/config.cpython-310.pyc and b/project/__pycache__/config.cpython-310.pyc differ
 
project/bot/__pycache__/openai_backend.cpython-310.pyc CHANGED
Binary files a/project/bot/__pycache__/openai_backend.cpython-310.pyc and b/project/bot/__pycache__/openai_backend.cpython-310.pyc differ
 
project/bot/documents.py CHANGED
@@ -3,33 +3,33 @@ import pandas as pd
3
 
4
  with open('../../data.json', 'r') as f:
5
  data = json.load(f)
6
-
7
  chunks = []
8
- for post in data:
9
- post_text = post['text']
10
- comments: list[dict] = post['comments']
11
- comments_str = ''
12
- for i, comment in enumerate(comments):
13
- comment_text = list(comment.keys())[0]
14
- replies = comment[comment_text]
15
- reply_str = 'Replies:\n'
16
- for j, reply in enumerate(replies):
17
- if j + 1 == len(replies):
18
- reply_str += f' • {reply}'
19
- else:
20
- reply_str += f' • {reply}\n'
21
- comments_str += f'{i + 1}. {comment_text}\n'
22
- if replies:
23
- comments_str += f'{reply_str}\n'
24
-
25
- chunk = f"Post: {post_text}\n"
26
- if comments:
27
- chunk += f'Comments:\n{comments_str}'
28
- chunks.append(chunk)
 
 
 
29
  #
30
- df = pd.DataFrame({"chunks": chunks})
31
- df.to_csv('chunks_javea.csv', index=False)
32
-
33
  # for post in data:
34
  # post_text = post['text']
35
  # comments: list[dict] = post['comments']
@@ -54,3 +54,30 @@ df.to_csv('chunks_javea.csv', index=False)
54
 
55
  # df = pd.DataFrame({"chunks": chunks})
56
  # df.to_csv('chunks_javea_raw.csv', index=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  with open('../../data.json', 'r') as f:
5
  data = json.load(f)
6
+ #
7
  chunks = []
8
+ # for post in data:
9
+ # post_text = post['text']
10
+ # comments: list[dict] = post['comments']
11
+ # comments_str = ''
12
+ # for i, comment in enumerate(comments):
13
+ # comment_text = list(comment.keys())[0]
14
+ # replies = comment[comment_text]
15
+ # reply_str = 'Replies:\n'
16
+ # for j, reply in enumerate(replies):
17
+ # if j + 1 == len(replies):
18
+ # reply_str += f' • {reply}'
19
+ # else:
20
+ # reply_str += f' • {reply}\n'
21
+ # comments_str += f'{i + 1}. {comment_text}\n'
22
+ # if replies:
23
+ # comments_str += f'{reply_str}\n'
24
+ #
25
+ # chunk = f"Post: {post_text}\n"
26
+ # if comments:
27
+ # chunk += f'Comments:\n{comments_str}'
28
+ # chunks.append(chunk)
29
+ # #
30
+ # df = pd.DataFrame({"chunks": chunks})
31
+ # df.to_csv('chunks_javea.csv', index=False)
32
  #
 
 
 
33
  # for post in data:
34
  # post_text = post['text']
35
  # comments: list[dict] = post['comments']
 
54
 
55
  # df = pd.DataFrame({"chunks": chunks})
56
  # df.to_csv('chunks_javea_raw.csv', index=False)
57
+
58
+ # import requests
59
+ # import json
60
+ #
61
+ # # URL для запроса к API
62
+ # url = "https://places.googleapis.com/v1/places:searchText"
63
+ #
64
+ # # API ключ (замените 'API_KEY' на ваш реальный API-ключ)
65
+ # api_key = "AIzaSyAcfgJAVTK1gwC9tmeKg-wcVWCy5CdieW4"
66
+ #
67
+ # # Заголовки запроса
68
+ # headers = {
69
+ # "Content-Type": "application/json",
70
+ # "X-Goog-Api-Key": api_key,
71
+ # "X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.priceLevel"
72
+ # }
73
+ #
74
+ # # Тело запроса
75
+ # data = {
76
+ # "textQuery": "Spicy Vegetarian Food in Sydney, Australia"
77
+ # }
78
+ #
79
+ # # Отправка POST запроса
80
+ # response = requests.post(url, headers=headers, data=json.dumps(data))
81
+ #
82
+ # # Вывод ответа от сервера
83
+ # print(response.text)
project/bot/openai_backend.py CHANGED
@@ -1,10 +1,14 @@
1
  import asyncio
 
 
2
  from typing import List, Dict
3
  import faiss
 
4
  import numpy as np
5
  import pandas as pd
6
  from sqlalchemy.ext.asyncio import AsyncSession
7
  from starlette.websockets import WebSocket
 
8
 
9
  from project.bot.models import MessagePair
10
  from project.config import settings
@@ -12,6 +16,7 @@ from project.config import settings
12
 
13
  class SearchBot:
14
  chat_history = []
 
15
  # is_unknown = False
16
  # unknown_counter = 0
17
 
@@ -20,33 +25,70 @@ class SearchBot:
20
  memory = []
21
  self.chat_history = memory
22
 
23
- async def _summarize_user_intent(self, user_query: str) -> str:
24
- chat_history_str = ''
25
- chat_history = self.chat_history[-self.unknown_counter * 2:]
26
- for i in chat_history:
27
- if i['role'] == 'user':
28
- chat_history_str += f"{i['role']}: {i['content']}\n"
29
- messages = [
30
- {
31
- 'role': 'system',
32
- 'content': f"{settings.SUMMARIZE_PROMPT}\n"
33
- f"Chat history: ```{chat_history_str}```\n"
34
- f"User query: ```{user_query}```"
35
- }
36
- ]
37
- response = await settings.OPENAI_CLIENT.chat.completions.create(
38
- messages=messages,
39
- temperature=0.1,
40
- n=1,
41
- model="gpt-3.5-turbo-0125"
42
- )
43
- user_intent = response.choices[0].message.content
44
- return user_intent
45
-
46
  @staticmethod
47
  def _cls_pooling(model_output):
48
  return model_output.last_hidden_state[:, 0]
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  async def _convert_to_embeddings(self, text_list):
51
  encoded_input = settings.INFO_TOKENIZER(
52
  text_list, padding=True, truncation=True, return_tensors="pt"
@@ -57,7 +99,7 @@ class SearchBot:
57
 
58
  @staticmethod
59
  async def _get_context_data(user_query: list[float]) -> list[dict]:
60
- radius = 30
61
  _, distances, indices = settings.FAISS_INDEX.range_search(user_query, radius)
62
  indices_distances_df = pd.DataFrame({'index': indices, 'distance': distances})
63
  filtered_data_df = settings.products_dataset.iloc[indices].copy()
@@ -83,7 +125,6 @@ class SearchBot:
83
  else:
84
  content = settings.EMPTY_PROMPT
85
  user_message = {"role": 'user', "content": query}
86
-
87
  self.chat_history.append(user_message)
88
  messages = [
89
  {
@@ -122,7 +163,8 @@ class SearchBot:
122
  try:
123
  async for chunk in self._rag(context, query, session, country):
124
  await websocket.send_text(chunk)
125
- # await websocket.send_text('finish')
 
126
  except Exception:
127
  await self.emergency_db_saving(session)
128
 
 
1
  import asyncio
2
+ import json
3
+ import re
4
  from typing import List, Dict
5
  import faiss
6
+ import httpx
7
  import numpy as np
8
  import pandas as pd
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
  from starlette.websockets import WebSocket
11
+ from transformers import pipeline
12
 
13
  from project.bot.models import MessagePair
14
  from project.config import settings
 
16
 
17
  class SearchBot:
18
  chat_history = []
19
+
20
  # is_unknown = False
21
  # unknown_counter = 0
22
 
 
25
  memory = []
26
  self.chat_history = memory
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  @staticmethod
29
  def _cls_pooling(model_output):
30
  return model_output.last_hidden_state[:, 0]
31
 
32
+ @staticmethod
33
+ async def enrich_information_from_google(search_word: str) -> str:
34
+ url = "https://places.googleapis.com/v1/places:searchText"
35
+ headers = {
36
+ "Content-Type": "application/json",
37
+ "X-Goog-Api-Key": settings.GOOGLE_PLACES_API_KEY,
38
+ "X-Goog-FieldMask": "places.shortFormattedAddress,places.websiteUri,places.internationalPhoneNumber,"
39
+ "places.googleMapsUri,places.photos"
40
+ }
41
+ data = {
42
+ "textQuery": f"{search_word} in Javea",
43
+ "languageCode": "nl",
44
+ "maxResultCount": 1,
45
+
46
+ }
47
+ async with httpx.AsyncClient() as client:
48
+ response = await client.post(url, headers=headers, content=json.dumps(data))
49
+ place_response = response.json()
50
+ place_response = place_response['places'][0]
51
+ photo_name = place_response.get('photos')
52
+ photo_uri = None
53
+ if photo_name:
54
+ async with httpx.AsyncClient() as client:
55
+ response = await client.get(
56
+ f'https://places.googleapis.com/v1/{photo_name[0]["name"]}/media?maxWidthPx=350&key={settings.GOOGLE_PLACES_API_KEY}')
57
+ photo_response = response.json()
58
+ photo_uri = photo_response.get('photoUri')
59
+ google_maps_uri = place_response.get('googleMapsUri')
60
+ phone_number = place_response.get('internationalPhoneNumber')
61
+ formatted_address = place_response.get('shortFormattedAddress')
62
+ website_uri = place_response.get('websiteUri')
63
+ if not google_maps_uri:
64
+ return search_word
65
+ enriched_word = f'<a class="extraDataLink" href="{google_maps_uri}" target="_blank">{search_word}</a><div class="tooltip-elem">'
66
+ if photo_uri:
67
+ enriched_word += f'<img src="{photo_uri}" alt="Image" class="tooltip-img">'
68
+ if formatted_address:
69
+ enriched_word += f'<p><a href="{google_maps_uri}" target="_blank">{formatted_address}</a></p>'
70
+ if website_uri:
71
+ enriched_word += f'<p><a href="{website_uri}">Google Maps URI</a></p>'
72
+ if phone_number:
73
+ phone_str = re.sub(r' ', '', phone_number)
74
+ enriched_word += f'<p><a href="tel:{phone_str}">Phone number</a></p>'
75
+ enriched_word += f"</div>"
76
+ return enriched_word
77
+
78
+ async def analyze_full_response(self) -> str:
79
+ assistant_message = self.chat_history.pop()['content']
80
+ nlp = pipeline("ner", model=settings.NLP_MODEL, tokenizer=settings.NLP_TOKENIZER, grouped_entities=True)
81
+ ner_result = nlp(assistant_message)
82
+ analyzed_assistant_message = assistant_message
83
+ for entity in ner_result:
84
+ if entity['entity_group'] in ("LOC", "ORG", "MISC") and entity['word'] != "Javea":
85
+ start = entity['start']
86
+ end = entity['end']
87
+ enriched_information = await self.enrich_information_from_google(entity['word'])
88
+ analyzed_assistant_message = analyzed_assistant_message[
89
+ :start] + f"{enriched_information}" + analyzed_assistant_message[end:]
90
+ return "ENRICHED:" + analyzed_assistant_message
91
+
92
  async def _convert_to_embeddings(self, text_list):
93
  encoded_input = settings.INFO_TOKENIZER(
94
  text_list, padding=True, truncation=True, return_tensors="pt"
 
99
 
100
  @staticmethod
101
  async def _get_context_data(user_query: list[float]) -> list[dict]:
102
+ radius = 4
103
  _, distances, indices = settings.FAISS_INDEX.range_search(user_query, radius)
104
  indices_distances_df = pd.DataFrame({'index': indices, 'distance': distances})
105
  filtered_data_df = settings.products_dataset.iloc[indices].copy()
 
125
  else:
126
  content = settings.EMPTY_PROMPT
127
  user_message = {"role": 'user', "content": query}
 
128
  self.chat_history.append(user_message)
129
  messages = [
130
  {
 
163
  try:
164
  async for chunk in self._rag(context, query, session, country):
165
  await websocket.send_text(chunk)
166
+ analyzing = await self.analyze_full_response()
167
+ await websocket.send_text(analyzing)
168
  except Exception:
169
  await self.emergency_db_saving(session)
170
 
project/bot/templates/home.html CHANGED
@@ -12,8 +12,8 @@
12
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
13
  integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
14
  <!-- Connect style.css -->
15
- <!-- <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}"> -->
16
- <link rel="stylesheet" href="https://brestok-javeaai.hf.space/static/css/style.css">
17
  </head>
18
 
19
  <div class="container-fluid px-0">
@@ -44,9 +44,7 @@
44
  <div class="chat-body" id="chatBody">
45
  <div class="message">
46
  <div class="bot_message mt-4 py-3 px-4 rounded-4 w-75 ms-3">
47
- Hallo! Ik ben een virtuele assistent, een helper voor Nederlanders die naar Javea zijn
48
- gemigreerd. Ik weet alles over deze regio en zal je helpen om je aan te passen in je nieuwe
49
- thuis. Waarmee kan ik je helpen?
50
  </div>
51
  </div>
52
  </div>
@@ -64,6 +62,7 @@
64
  </div>
65
  </div>
66
 
 
67
  </html>
68
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
69
  integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
@@ -74,9 +73,9 @@
74
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
75
  <script src="https://kit.fontawesome.com/d4ffd37f75.js" crossorigin="anonymous"></script>
76
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
77
- <script type="text/javascript" src="https://brestok-javeaai.hf.space/static/js/main.js"></script>
78
- <script type="text/javascript" src="https://brestok-javeaai.hf.space/static/js/utils.js"></script>
79
- <script type="text/javascript" src="https://brestok-javeaai.hf.space/static/js/ws.js"></script>
80
- <!-- <script type="text/javascript" src="{{ url_for('static', path='/js/main.js') }}"></script> -->
81
- <!-- <script type="text/javascript" src="{{ url_for('static', path='/js/utils.js') }}"></script> -->
82
- <!-- <script type="text/javascript" src="{{ url_for('static', path='/js/ws.js') }}"></script> -->
 
12
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
13
  integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
14
  <!-- Connect style.css -->
15
+ <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
16
+ <!-- <link rel="stylesheet" href="../../../static/css/style.css">-->
17
  </head>
18
 
19
  <div class="container-fluid px-0">
 
44
  <div class="chat-body" id="chatBody">
45
  <div class="message">
46
  <div class="bot_message mt-4 py-3 px-4 rounded-4 w-75 ms-3">
47
+ Hallo! Ich bin der virtuelle Assistent Alcolm AI. Sie können sich jederzeit mit Fragen zum Alcolm-Software an mich wenden. Ich helfe Ihnen gerne weiter!
 
 
48
  </div>
49
  </div>
50
  </div>
 
62
  </div>
63
  </div>
64
 
65
+
66
  </html>
67
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
68
  integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
 
73
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
74
  <script src="https://kit.fontawesome.com/d4ffd37f75.js" crossorigin="anonymous"></script>
75
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
76
+ <!--<script type="text/javascript" src="../../../static/js/main.js"></script>-->
77
+ <!--<script type="text/javascript" src="../../../static/js/utils.js"></script>-->
78
+ <!--<script type="text/javascript" src="../../../static/js/ws.js"></script>-->
79
+ <script type="text/javascript" src="{{ url_for('static', path='/js/main.js') }}"></script>
80
+ <script type="text/javascript" src="{{ url_for('static', path='/js/utils.js') }}"></script>
81
+ <script type="text/javascript" src="{{ url_for('static', path='/js/ws.js') }}"></script>
project/config.py CHANGED
@@ -6,7 +6,7 @@ from openai import AsyncOpenAI
6
  import pathlib
7
  from functools import lru_cache
8
  from environs import Env
9
- from transformers import AutoModel, AutoTokenizer
10
  import torch
11
 
12
  env = Env()
@@ -21,6 +21,9 @@ class BaseConfig:
21
  INFO_TOKENIZER = AutoTokenizer.from_pretrained(MODEL_NAME)
22
  OPENAI_CLIENT = AsyncOpenAI(api_key=os.getenv('OPENAI_API_KEY'))
23
  FAISS_INDEX = faiss.read_index(str(BASE_DIR / 'faiss_javea.index'))
 
 
 
24
 
25
 
26
  class DevelopmentConfig(BaseConfig):
@@ -33,24 +36,21 @@ class ProductionConfig(BaseConfig):
33
  f"{env('DATABASE_HOST')}:" \
34
  f"{env('DATABASE_PORT')}/" \
35
  f"{env('DATABASE_NAME')}"
36
- PROMPT = "Je bent een expert in de regio Javea in Italië, die alles weet om mensen te helpen die migreren van " \
37
- "Nederland naar Spanje. Je taak is om mensen te helpen zich te vestigen in de nieuwe stad. Gebruik " \
38
- "kennis uit je vorige antwoord (voornamelijk uit opmerkingen) om een informatief antwoord " \
39
- "te geven op de gebruikersvraag. Vermeld nooit dat je kennis haalt uit posts of opmerkingen. Spreek " \
40
- "vanuit jezelf."
 
41
  EMPTY_PROMPT = "Je bent een expert in Javea aan de Costa Blanca in Spanje, met uitgebreide kennis om Nederlanders " \
42
  "te helpen die naar deze regio verhuizen. Je taak is om mensen te helpen zich thuis te voelen in " \
43
  "hun nieuwe stad. Gebruik je kennis over deze regio maar informatieve antwoorden te " \
44
  "geven op de vragen van gebruikers."
45
- SUMMARIZE_PROMPT = "Study the user's requests, paying special attention to the specific mentioned wishes when " \
46
- "choosing a house. Combine these details into a single query that reflects all the user's " \
47
- "needs. Formulate your answer as if you were a user, clearly and concisely stating the " \
48
- "requirements. Make sure that all relevant user wishes are indicated in your response. "
49
 
50
  def __init__(self):
51
  if torch.cuda.is_available():
52
  device = torch.device("cuda")
53
-
54
  else:
55
  device = torch.device("cpu")
56
  self.device = device
 
6
  import pathlib
7
  from functools import lru_cache
8
  from environs import Env
9
+ from transformers import AutoModel, AutoTokenizer, AutoModelForTokenClassification
10
  import torch
11
 
12
  env = Env()
 
21
  INFO_TOKENIZER = AutoTokenizer.from_pretrained(MODEL_NAME)
22
  OPENAI_CLIENT = AsyncOpenAI(api_key=os.getenv('OPENAI_API_KEY'))
23
  FAISS_INDEX = faiss.read_index(str(BASE_DIR / 'faiss_javea.index'))
24
+ NLP_MODEL_NAME = 'Babelscape/wikineural-multilingual-ner'
25
+ NLP_TOKENIZER = AutoTokenizer.from_pretrained("Babelscape/wikineural-multilingual-ner")
26
+ NLP_MODEL = AutoModelForTokenClassification.from_pretrained("Babelscape/wikineural-multilingual-ner")
27
 
28
 
29
  class DevelopmentConfig(BaseConfig):
 
36
  f"{env('DATABASE_HOST')}:" \
37
  f"{env('DATABASE_PORT')}/" \
38
  f"{env('DATABASE_NAME')}"
39
+ PROMPT = "Je bent een expert in de Spaanse regio Javea die alles weet over hoe je mensen kunt helpen die vanuit " \
40
+ "Nederland naar Spanje migreren. Jouw taak is om mensen te helpen zich te vestigen in een nieuwe stad. " \
41
+ "Gebruik de kennis die je in je vorige antwoord hebt opgedaan (meestal uit opmerkingen) om een " \
42
+ "informatief antwoord te geven op de vraag van de gebruiker. Concentreer je in je antwoorden op de namen " \
43
+ "van de locaties. Vermeld nooit dat je kennis afkomstig is van berichten of opmerkingen. Spreek namens " \
44
+ "jezelf. "
45
  EMPTY_PROMPT = "Je bent een expert in Javea aan de Costa Blanca in Spanje, met uitgebreide kennis om Nederlanders " \
46
  "te helpen die naar deze regio verhuizen. Je taak is om mensen te helpen zich thuis te voelen in " \
47
  "hun nieuwe stad. Gebruik je kennis over deze regio maar informatieve antwoorden te " \
48
  "geven op de vragen van gebruikers."
49
+ GOOGLE_PLACES_API_KEY = env('GOOGLE_PLACES_API_KEY')
 
 
 
50
 
51
  def __init__(self):
52
  if torch.cuda.is_available():
53
  device = torch.device("cuda")
 
54
  else:
55
  device = torch.device("cpu")
56
  self.device = device
static/css/style.css CHANGED
@@ -11,6 +11,8 @@ body {
11
  .chat-header {
12
  background-color: #f1f1f1;
13
  border-bottom: 1px solid #bdbdbd;
 
 
14
  }
15
 
16
  .chat-body {
@@ -79,3 +81,39 @@ body {
79
  padding: 20px;
80
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
81
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  .chat-header {
12
  background-color: #f1f1f1;
13
  border-bottom: 1px solid #bdbdbd;
14
+ /*z-index: 10;*/
15
+ /*position: relative;*/
16
  }
17
 
18
  .chat-body {
 
81
  padding: 20px;
82
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
83
  }
84
+
85
+ .extraDataLink {
86
+ color: #105ead;
87
+ text-decoration: none;
88
+ position: relative;
89
+ }
90
+
91
+ .extraDataLink:hover {
92
+ color: #1164b7;
93
+ text-decoration: none;
94
+ z-index: 150;
95
+ }
96
+
97
+ .tooltip-elem {
98
+ position: absolute;
99
+ background-color: black;
100
+ color: white;
101
+ font-size: 12px;
102
+ padding: 8px;
103
+ border-radius: 4px;
104
+ z-index: 1000;
105
+ display: none;
106
+ }
107
+ .extraDataLink + .tooltip-elem {
108
+ position: absolute;
109
+ }
110
+
111
+ .tooltip-img{
112
+ border-radius: 4px;
113
+ margin-bottom: 5px;
114
+ }
115
+
116
+ .tooltip-elem a{
117
+ text-decoration: none;
118
+ color: #008dff;
119
+ }
static/js/main.js CHANGED
@@ -21,7 +21,6 @@ function adjustChatBodyHeight() {
21
  const chatHeader = document.getElementById('chatHeader')
22
  const chatFooterHeight = getTotalHeight(chatFooter);
23
  const chatHeaderHeight = getTotalHeight(chatHeader);
24
- const openButtonWindow = getTotalHeight(openButton)
25
  const viewportHeight = window.innerHeight - chatHeaderHeight - chatFooterHeight;
26
  chatBody.style.height = viewportHeight + 'px';
27
  }
@@ -29,7 +28,7 @@ function adjustChatBodyHeight() {
29
  function openChatBotWindow() {
30
  let lastScrollHeight = chatBody.scrollHeight;
31
  const uuid = generateUUID()
32
- socket = new WebSocket(`wss://brestok-javeaai.hf.space/ws/${uuid}`);
33
  socket.onclose = (event) => console.log('WebSocket disconnected', event);
34
  socket.onerror = (error) => {
35
  alert('Something was wrong. Try again later.')
@@ -46,7 +45,13 @@ function openChatBotWindow() {
46
  const botMessages = document.querySelectorAll('.bot_message');
47
  botMessage = botMessages[botMessages.length - 1];
48
  } else {
49
- botMessage.innerHTML = marked.parse(event.data)
 
 
 
 
 
 
50
  }
51
  }
52
  }
@@ -61,4 +66,4 @@ openButton.addEventListener('click', function () {
61
  chatbotWindow.style.visibility = "visible";
62
  openButton.style.display = 'none'
63
  openChatBotWindow();
64
- });
 
21
  const chatHeader = document.getElementById('chatHeader')
22
  const chatFooterHeight = getTotalHeight(chatFooter);
23
  const chatHeaderHeight = getTotalHeight(chatHeader);
 
24
  const viewportHeight = window.innerHeight - chatHeaderHeight - chatFooterHeight;
25
  chatBody.style.height = viewportHeight + 'px';
26
  }
 
28
  function openChatBotWindow() {
29
  let lastScrollHeight = chatBody.scrollHeight;
30
  const uuid = generateUUID()
31
+ socket = new WebSocket(`ws://127.0.0.1:8000/ws/${uuid}`);
32
  socket.onclose = (event) => console.log('WebSocket disconnected', event);
33
  socket.onerror = (error) => {
34
  alert('Something was wrong. Try again later.')
 
45
  const botMessages = document.querySelectorAll('.bot_message');
46
  botMessage = botMessages[botMessages.length - 1];
47
  } else {
48
+ const aiMessage = event.data
49
+ if (aiMessage.startsWith('ENRICHED:')) {
50
+ botMessage.innerHTML = aiMessage.substring(9)
51
+ enrichAIResponse(botMessage)
52
+ } else {
53
+ botMessage.innerHTML = marked.parse(aiMessage)
54
+ }
55
  }
56
  }
57
  }
 
66
  chatbotWindow.style.visibility = "visible";
67
  openButton.style.display = 'none'
68
  openChatBotWindow();
69
+ });
static/js/utils.js CHANGED
@@ -5,6 +5,7 @@ function createNewMessage(text, type) {
5
  const message = document.createElement('div')
6
  message.className = 'message'
7
  let content = document.createElement('div')
 
8
  if (type === 'bot') {
9
  content.className = 'bot_message mt-4 py-3 px-4 rounded-4 w-75 ms-3'
10
  } else {
@@ -45,4 +46,42 @@ textInput.addEventListener("keydown", function (event) {
45
  }
46
  });
47
 
48
- sendButton.addEventListener("click", sendMessageToServer);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  const message = document.createElement('div')
6
  message.className = 'message'
7
  let content = document.createElement('div')
8
+ content.style.whiteSpace = "pre-line"
9
  if (type === 'bot') {
10
  content.className = 'bot_message mt-4 py-3 px-4 rounded-4 w-75 ms-3'
11
  } else {
 
46
  }
47
  });
48
 
49
+ sendButton.addEventListener("click", sendMessageToServer);
50
+
51
+
52
+ function enrichAIResponse(botMessageElement) {
53
+ const links = botMessageElement.querySelectorAll('.extraDataLink');
54
+
55
+ links.forEach(link => {
56
+ const tooltip = link.nextElementSibling;
57
+ link.addEventListener('mouseenter', function () {
58
+ tooltip.style.display = 'block';
59
+ const tooltipWidth = parseInt(link.offsetWidth) * 2
60
+ tooltip.style.width = tooltipWidth + 'px'
61
+
62
+ const tooltipImg = tooltip.querySelector('.tooltip-img')
63
+ if (tooltipImg) {
64
+ tooltipImg.width = tooltipWidth - 16
65
+ }
66
+ const linkRect = this.getBoundingClientRect();
67
+ if (linkRect.top < tooltip.offsetHeight + 4) {
68
+ tooltip.style.top = (window.scrollY + linkRect.bottom + 4) + 'px';
69
+ } else {
70
+ tooltip.style.top = (window.scrollY + linkRect.top - tooltip.offsetHeight - 4) + 'px';
71
+ }
72
+ tooltip.style.left = (linkRect.left + (linkRect.width / 2) - (tooltipWidth / 2)) + 'px';
73
+ });
74
+
75
+ link.addEventListener('mouseleave', function () {
76
+ setTimeout(() => {
77
+ if (!tooltip.matches(':hover')) {
78
+ tooltip.style.display = 'none';
79
+ }
80
+ }, 300)
81
+ });
82
+
83
+ tooltip.addEventListener('mouseleave', function () {
84
+ this.style.display = 'none';
85
+ });
86
+ });
87
+ }