AnatoliiG commited on
Commit
e8fef7f
·
1 Parent(s): 14bdcff

refactor frontend

Browse files
Files changed (3) hide show
  1. src/ui/callbacks.py +39 -75
  2. src/ui/components.py +37 -41
  3. src/ui/styles.py +22 -176
src/ui/callbacks.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import os
2
  import re
3
  import urllib.parse
@@ -10,9 +12,6 @@ from src.utils.helpers import get_clean_text
10
 
11
 
12
  def user_input(user_message, history):
13
- """
14
- Добавляет сообщение пользователя в историю и очищает поле ввода.
15
- """
16
  if not user_message:
17
  return None, history
18
  history = history or []
@@ -21,56 +20,43 @@ def user_input(user_message, history):
21
 
22
 
23
  def set_interactive(is_interactive):
24
- """
25
- Блокирует/разблокирует интерфейс во время работы Агента.
26
- """
27
  return (
28
  gr.update(
29
  interactive=is_interactive,
30
  placeholder="Ожидайте, Агент думает..."
31
  if not is_interactive
32
- else "Напиши сообщение или вопрос...",
33
  ),
34
  gr.update(interactive=is_interactive),
35
  )
36
 
37
 
38
  def web_search(query: str, max_results: int = 3) -> list:
39
- """
40
- Выполняет реальный поиск в DuckDuckGo через HTML-версию
41
- без тяжелых внешних зависимостей.
42
- """
43
  headers = {
44
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
45
  }
46
  url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
47
  try:
48
  response = httpx.get(url, headers=headers, timeout=5.0)
49
  if response.status_code != 200:
50
  return []
51
-
52
  html = response.text
53
- # Парсим ссылки и сниппеты с помощью регулярных выражений
54
  titles = re.findall(r'<a class="result__a" href="([^"]+)">([^<]+)</a>', html)
55
  snippets = re.findall(r'<td class="result-snippet">([\s\S]*?)</td>', html)
56
-
57
  results = []
58
  for i in range(min(len(titles), len(snippets), max_results)):
59
- # Очищаем URL от редиректов DuckDuckGo
60
  url_match = re.search(r"uddg=([^&]+)", titles[i][0])
61
- raw_url = titles[i][0]
62
- if url_match:
63
- raw_url = urllib.parse.unquote(url_match.group(1))
64
-
65
- # Удаляем HTML теги из сниппета для чистого текста
66
- snippet_clean = re.sub(r"<[^>]+>", "", snippets[i]).strip()
67
  snippet_clean = (
68
- snippet_clean.replace("&amp;", "&")
 
 
69
  .replace("&quot;", '"')
70
  .replace("&lt;", "<")
71
  .replace("&gt;", ">")
72
  )
73
-
74
  results.append(
75
  {
76
  "title": titles[i][1].strip(),
@@ -80,59 +66,46 @@ def web_search(query: str, max_results: int = 3) -> list:
80
  )
81
  return results
82
  except Exception as e:
83
- print(f"Ошибка поиска DuckDuckGo: {e}")
84
  return []
85
 
86
 
87
  def bot_response(
88
  history, system_prompt, temperature, max_tokens, use_search, uploaded_file
89
  ):
90
- """
91
- Основной воркер Агента: считывает файлы, ищет в сети и запускает LLM.
92
- """
93
  messages = [{"role": "system", "content": system_prompt}]
 
94
 
95
- # 1. Чтение файла (если загружен)
96
- file_info = ""
97
- file_content = ""
98
  if uploaded_file and os.path.exists(uploaded_file):
99
  filename = os.path.basename(uploaded_file)
100
  size_kb = os.path.getsize(uploaded_file) / 1024
101
  file_info = f"📎 **Файл прочитан:** `{filename}` ({size_kb:.1f} KB)"
102
-
103
  try:
104
  with open(uploaded_file, "r", encoding="utf-8", errors="ignore") as f:
105
- # Ограничиваем считывание 40k символов для избежания перегрузки контекста
106
  file_content = f.read(40000)
107
  if len(file_content) >= 40000:
108
- file_info += " *(загружен первый сегмент файла)*"
109
  except Exception as e:
110
- file_content = f"[Ошибка чтения файла: {e}]"
111
- file_info = f"❌ **Ошибка при чтении файла:** `{filename}` ({e})"
112
 
113
- # 2. Формирование контекста диалога (последние 7 реплик)
114
  for msg in history[-7:]:
115
  messages.append(
116
  {"role": msg["role"], "content": get_clean_text(msg["content"])}
117
  )
118
 
119
- # Добавляем в историю системную плашку-статус
120
- history.append({"role": "assistant", "content": "⏳ Инициализация агента..."})
121
  yield history
122
 
123
- # 3. Работа веб-поиска
124
  search_info = ""
125
  if use_search:
126
- user_query = ""
127
- # Берем последний запрос пользователя в качестве поискового запроса
128
- for msg in reversed(messages):
129
- if msg["role"] == "user":
130
- user_query = msg["content"]
131
- break
132
-
133
  if user_query:
134
- search_info = f'🌐 **Поиск в интернете:** Выполняется поиск по запросу *"{user_query}"*...'
135
- # Обновляем сообщение в интерфейсе для прозрачности действий
136
  history[-1]["content"] = (
137
  f"{file_info + '\n' if file_info else ''}{search_info}"
138
  )
@@ -140,40 +113,36 @@ def bot_response(
140
 
141
  search_results = web_search(user_query)
142
  if search_results:
143
- search_info = f'🌐 **Поиск в интернете:** Найдено {len(search_results)} результатов по запросу *"{user_query}"*'
144
- search_context = "Результаты поиска из интернета для вашего ответа:\n\n"
145
- for idx, r in enumerate(search_results, 1):
146
- search_context += (
147
- f"{idx}. {r['title']} ({r['url']})\nСниппет: {r['snippet']}\n\n"
148
- )
149
-
150
- # Помещаем результаты поиска в системный контекст модели
151
  messages.append({"role": "system", "content": search_context})
152
  else:
153
- search_info = f'🌐 **Поиск в интернете:** По запросу *"{user_query}"* результатов не найдено.'
154
 
155
- # Если файл прочитан, инжектируем его содержимое в контекст
156
  if file_content:
157
  messages.append(
158
  {
159
  "role": "system",
160
- "content": f"Пользователь предоставил вам файл '{os.path.basename(uploaded_file)}'. Его содержимое:\n\n{file_content}",
161
  }
162
  )
163
 
164
- # Формируем итоговую визуальную шапку статусов перед ответом
165
- status_header = ""
166
- if file_info:
167
- status_header += f"{file_info}\n"
168
- if search_info:
169
- status_header += f"{search_info}\n"
170
  if status_header:
171
- status_header += "\n---\n\n"
172
 
173
- history[-1]["content"] = status_header + "⏳ Генерация ответа моделью..."
174
  yield history
175
 
176
- # 4. Стриминг ответа модели
177
  try:
178
  stream = engine.generate(
179
  messages=messages,
@@ -181,18 +150,13 @@ def bot_response(
181
  temperature=temperature,
182
  stream=True,
183
  )
184
-
185
  partial_text = ""
186
  for chunk in stream:
187
  delta = chunk["choices"][0].get("delta", {})
188
  if delta.get("content"):
189
  partial_text += delta["content"]
190
- # Выводим ответ модели сразу под нашей шапкой статуса
191
  history[-1]["content"] = status_header + partial_text
192
  yield history
193
-
194
  except Exception as e:
195
- history[-1]["content"] = (
196
- status_header + f"\n\n❌ Ошибка во время генерации: {str(e)}"
197
- )
198
  yield history
 
1
+ # src/ui/callbacks.py
2
+
3
  import os
4
  import re
5
  import urllib.parse
 
12
 
13
 
14
  def user_input(user_message, history):
 
 
 
15
  if not user_message:
16
  return None, history
17
  history = history or []
 
20
 
21
 
22
  def set_interactive(is_interactive):
 
 
 
23
  return (
24
  gr.update(
25
  interactive=is_interactive,
26
  placeholder="Ожидайте, Агент думает..."
27
  if not is_interactive
28
+ else "Напиши сообщение...",
29
  ),
30
  gr.update(interactive=is_interactive),
31
  )
32
 
33
 
34
  def web_search(query: str, max_results: int = 3) -> list:
 
 
 
 
35
  headers = {
36
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
37
  }
38
  url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
39
  try:
40
  response = httpx.get(url, headers=headers, timeout=5.0)
41
  if response.status_code != 200:
42
  return []
 
43
  html = response.text
 
44
  titles = re.findall(r'<a class="result__a" href="([^"]+)">([^<]+)</a>', html)
45
  snippets = re.findall(r'<td class="result-snippet">([\s\S]*?)</td>', html)
 
46
  results = []
47
  for i in range(min(len(titles), len(snippets), max_results)):
 
48
  url_match = re.search(r"uddg=([^&]+)", titles[i][0])
49
+ raw_url = (
50
+ urllib.parse.unquote(url_match.group(1)) if url_match else titles[i][0]
51
+ )
 
 
 
52
  snippet_clean = (
53
+ re.sub(r"<[^>]+>", "", snippets[i])
54
+ .strip()
55
+ .replace("&amp;", "&")
56
  .replace("&quot;", '"')
57
  .replace("&lt;", "<")
58
  .replace("&gt;", ">")
59
  )
 
60
  results.append(
61
  {
62
  "title": titles[i][1].strip(),
 
66
  )
67
  return results
68
  except Exception as e:
69
+ print(f"Ошибка поиска: {e}")
70
  return []
71
 
72
 
73
  def bot_response(
74
  history, system_prompt, temperature, max_tokens, use_search, uploaded_file
75
  ):
 
 
 
76
  messages = [{"role": "system", "content": system_prompt}]
77
+ file_info, file_content = "", ""
78
 
79
+ # 1. Чтение файла
 
 
80
  if uploaded_file and os.path.exists(uploaded_file):
81
  filename = os.path.basename(uploaded_file)
82
  size_kb = os.path.getsize(uploaded_file) / 1024
83
  file_info = f"📎 **Файл прочитан:** `{filename}` ({size_kb:.1f} KB)"
 
84
  try:
85
  with open(uploaded_file, "r", encoding="utf-8", errors="ignore") as f:
 
86
  file_content = f.read(40000)
87
  if len(file_content) >= 40000:
88
+ file_info += " *(загружен первый сегмент)*"
89
  except Exception as e:
90
+ file_info = f"❌ **Ошибка файла:** `{filename}` ({e})"
 
91
 
92
+ # 2. Формирование контекста диалога
93
  for msg in history[-7:]:
94
  messages.append(
95
  {"role": msg["role"], "content": get_clean_text(msg["content"])}
96
  )
97
 
98
+ history.append({"role": "assistant", "content": "⏳ Инициализация..."})
 
99
  yield history
100
 
101
+ # 3. Веб-поиск
102
  search_info = ""
103
  if use_search:
104
+ user_query = next(
105
+ (msg["content"] for msg in reversed(messages) if msg["role"] == "user"), ""
106
+ )
 
 
 
 
107
  if user_query:
108
+ search_info = f'🌐 Ищем: *"{user_query}"*...'
 
109
  history[-1]["content"] = (
110
  f"{file_info + '\n' if file_info else ''}{search_info}"
111
  )
 
113
 
114
  search_results = web_search(user_query)
115
  if search_results:
116
+ search_info = f"🌐 Найдено {len(search_results)} результатов."
117
+ search_context = "Результаты поиска:\n" + "".join(
118
+ [
119
+ f"{i}. {r['title']} ({r['url']})\n{r['snippet']}\n\n"
120
+ for i, r in enumerate(search_results, 1)
121
+ ]
122
+ )
 
123
  messages.append({"role": "system", "content": search_context})
124
  else:
125
+ search_info = "🌐 Ничего не найдено."
126
 
127
+ # Инжект файла
128
  if file_content:
129
  messages.append(
130
  {
131
  "role": "system",
132
+ "content": f"Файл '{os.path.basename(uploaded_file)}':\n\n{file_content}",
133
  }
134
  )
135
 
136
+ status_header = (file_info + "\n" if file_info else "") + (
137
+ search_info + "\n" if search_info else ""
138
+ )
 
 
 
139
  if status_header:
140
+ status_header += "---\n\n"
141
 
142
+ history[-1]["content"] = status_header + "⏳ Генерация ответа..."
143
  yield history
144
 
145
+ # 4. Стриминг ответа от LLM
146
  try:
147
  stream = engine.generate(
148
  messages=messages,
 
150
  temperature=temperature,
151
  stream=True,
152
  )
 
153
  partial_text = ""
154
  for chunk in stream:
155
  delta = chunk["choices"][0].get("delta", {})
156
  if delta.get("content"):
157
  partial_text += delta["content"]
 
158
  history[-1]["content"] = status_header + partial_text
159
  yield history
 
160
  except Exception as e:
161
+ history[-1]["content"] = status_header + f"\n\n❌ Ошибка: {str(e)}"
 
 
162
  yield history
src/ui/components.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import gradio as gr
2
 
3
  from src.core.config import settings
@@ -6,33 +8,25 @@ from src.ui.styles import CSS
6
 
7
 
8
  def create_ui():
9
- # Базовые блоки без конфликтующих флагов высоты
10
- with gr.Blocks(theme=gr.themes.Soft(), css=CSS, fill_height=True) as demo:
11
- gr.HTML(f"<style>{CSS}</style>")
12
- gr.HTML("<script>document.title = 'Code LLM Agent';</script>")
13
-
14
- with gr.Row(elem_id="main-row", variant="panel"):
15
- # Боковая панель
16
- with gr.Sidebar(elem_id="sidebar-container", width=280):
17
- gr.Markdown("## 🛠️ Dashboard")
18
 
19
- gr.Markdown("### 🧰 Agent Tools")
20
- use_search = gr.Checkbox(
21
- label="🌐 Включить поиск в интернете", value=False
22
- )
23
- uploaded_file = gr.File(
24
- label="📎 Загрузить файл",
25
- file_types=[".txt", ".pdf", ".py", ".md", ".json"],
26
- type="filepath",
27
- )
28
 
29
- gr.Markdown("### ⚙️ Model Settings")
 
30
  sys_pt = gr.Textbox(
31
  label="System Prompt",
32
- value="Вы опытный программист и AI-агент. Если нужно узнать свежую информацию, используйте поиск.",
33
- lines=4,
34
  )
35
- temp = gr.Slider(0, 1, value=settings.DEFAULT_TEMP, label="Temperature")
36
  tokens = gr.Slider(
37
  512,
38
  settings.CONTEXT_SIZE,
@@ -41,45 +35,47 @@ def create_ui():
41
  step=128,
42
  )
43
 
44
- gr.HTML(
45
- "<div style='margin: 15px 0; border-top: 1px solid var(--border-color-primary);'></div>"
46
- )
47
- clear = gr.Button("🗑️ Clear Chat", variant="stop")
 
 
 
 
 
48
 
49
- # Основная область чата
50
- with gr.Column(elem_id="col-chat-main"):
51
- chatbot = gr.Chatbot(
52
- elem_id="chatbot",
53
  show_label=False,
54
- scale=1, # <- Вот правильный способ растянуть чат
 
 
 
55
  )
 
56
 
57
- with gr.Row(elem_id="input-area"):
58
- msg = gr.Textbox(
59
- show_label=False,
60
- placeholder="Напиши сообщение или вопрос...",
61
- scale=10,
62
- autofocus=True,
63
- container=False,
64
- )
65
- submit = gr.Button("Run ➤", variant="primary", scale=1)
66
-
67
  input_args = [msg, chatbot]
68
  output_args = [msg, chatbot]
69
  gen_args = [chatbot, sys_pt, temp, tokens, use_search, uploaded_file]
70
 
 
71
  msg.submit(user_input, input_args, output_args, queue=False).then(
72
  lambda: set_interactive(False), None, [msg, submit]
73
  ).then(bot_response, gen_args, chatbot).then(
74
  lambda: set_interactive(True), None, [msg, submit]
75
  )
76
 
 
77
  submit.click(user_input, input_args, output_args, queue=False).then(
78
  lambda: set_interactive(False), None, [msg, submit]
79
  ).then(bot_response, gen_args, chatbot).then(
80
  lambda: set_interactive(True), None, [msg, submit]
81
  )
82
 
 
83
  clear.click(lambda: [], None, chatbot)
84
 
85
  return demo
 
1
+ # src/ui/components.py (или где у вас находится функция create_ui)
2
+
3
  import gradio as gr
4
 
5
  from src.core.config import settings
 
8
 
9
 
10
  def create_ui():
11
+ # fill_height=True растягивает интерфейс на всю высоту экрана
12
+ with gr.Blocks(theme=gr.themes.Base(), css=CSS, fill_height=True) as demo:
13
+ gr.HTML("<script>document.title = 'AI Agent';</script>")
 
 
 
 
 
 
14
 
15
+ with gr.Sidebar(width=280):
16
+ use_search = gr.Checkbox(label="🌐 Интернет-поиск", value=False)
17
+ uploaded_file = gr.File(
18
+ label="📎 Файл контекста",
19
+ file_types=[".txt", ".pdf", ".py", ".md", ".json"],
20
+ )
 
 
 
21
 
22
+ # Аккордеон скроет страшные ползунки, пока юзер сам их не откроет
23
+ with gr.Accordion("⚙️ Настройки модели", open=False):
24
  sys_pt = gr.Textbox(
25
  label="System Prompt",
26
+ value="Вы AI-агент. Если нужно узнать свежую информацию, используйте поиск.",
27
+ lines=3,
28
  )
29
+ temp = gr.Slider(0, 1, value=settings.DEFAULT_TEMP, label="Температура")
30
  tokens = gr.Slider(
31
  512,
32
  settings.CONTEXT_SIZE,
 
35
  step=128,
36
  )
37
 
38
+ clear = gr.Button("🗑️ Очистить чат")
39
+
40
+ # Чат
41
+ chatbot = gr.Chatbot(
42
+ elem_id="chatbot",
43
+ show_label=False,
44
+ scale=1, # Чат забирает всё свободное пространство сверху вниз
45
+ bubble_full_width=False, # Делает пузыри чата компактными (как в ChatGPT)
46
+ )
47
 
48
+ # Плашка ввода в самом низу
49
+ with gr.Row():
50
+ msg = gr.Textbox(
 
51
  show_label=False,
52
+ placeholder="Напиши сообщение...",
53
+ scale=10,
54
+ autofocus=True,
55
+ container=False,
56
  )
57
+ submit = gr.Button("➤", variant="primary", scale=1, min_width=50)
58
 
59
+ # События интерфейса (обработчики)
 
 
 
 
 
 
 
 
 
60
  input_args = [msg, chatbot]
61
  output_args = [msg, chatbot]
62
  gen_args = [chatbot, sys_pt, temp, tokens, use_search, uploaded_file]
63
 
64
+ # Обработка Enter в поле ввода
65
  msg.submit(user_input, input_args, output_args, queue=False).then(
66
  lambda: set_interactive(False), None, [msg, submit]
67
  ).then(bot_response, gen_args, chatbot).then(
68
  lambda: set_interactive(True), None, [msg, submit]
69
  )
70
 
71
+ # Обработка клика по кнопке отправки
72
  submit.click(user_input, input_args, output_args, queue=False).then(
73
  lambda: set_interactive(False), None, [msg, submit]
74
  ).then(bot_response, gen_args, chatbot).then(
75
  lambda: set_interactive(True), None, [msg, submit]
76
  )
77
 
78
+ # Очистка чата
79
  clear.click(lambda: [], None, chatbot)
80
 
81
  return demo
src/ui/styles.py CHANGED
@@ -1,208 +1,54 @@
1
- CSS = """
2
- /* 1. Глобальный фон страницы */
3
- html, body {
4
- background-color: #212121 !important;
5
- margin: 0 !important;
6
- padding: 0 !important;
7
- height: 100vh !important;
8
- overflow: hidden !important; /* Блокируем внешний скролл */
9
- }
10
-
11
- .gradio-container {
12
- background-color: #212121 !important;
13
- margin: 0 !important;
14
- padding: 0 !important;
15
- height: 100vh !important; /* Жестко 100% экрана */
16
- max-width: 100% !important;
17
- overflow: hidden !important;
18
- }
19
-
20
- /* 2. Главный ряд */
21
- #main-row {
22
- margin: 0 !important;
23
- gap: 0 !important;
24
- background-color: #212121 !important;
25
- height: 100% !important;
26
- flex-wrap: nowrap !important; /* Запрещаем перенос колонки чата под сайдбар */
27
- min-height: 0 !important;
28
- }
29
-
30
- /* 3. Боковая панель */
31
- #sidebar-container {
32
- background-color: #171717 !important;
33
- border-right: 1px solid #2f2f2f !important;
34
- padding: 1.5rem 1.25rem !important;
35
- height: 100% !important;
36
- overflow-y: auto !important; /* Включаем скролл внутри сайдбара, если много настроек */
37
- }
38
-
39
- /* Кнопки сайдбара оставляем как у вас... */
40
- .sidebar-parent button.toggle-button,
41
- .sidebar-parent > button {
42
- background-color: #171717 !important;
43
- color: #ececec !important;
44
- border: 1px solid #3a3a3a !important;
45
- border-radius: 50% !important;
46
- width: 36px !important;
47
- height: 36px !important;
48
- display: flex !important;
49
- align-items: center !important;
50
- justify-content: center !important;
51
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
52
- z-index: 999 !important;
53
- cursor: pointer !important;
54
- }
55
-
56
- .sidebar-parent button.toggle-button:hover,
57
- .sidebar-parent > button:hover {
58
- background-color: #2f2f2f !important;
59
- }
60
 
61
- /* 4. Колонка чата */
62
- #col-chat-main {
 
63
  background-color: #212121 !important;
64
- padding: 0 !important;
65
- margin: 0 !important;
66
- display: flex !important;
67
- flex-direction: column !important;
68
- height: 100% !important;
69
- min-height: 0 !important; /* КРИТИЧЕСКИ ВАЖНО: защита от расширения колонки */
70
  }
71
 
72
- /* Стилизация компонента Chatbot */
73
  #chatbot {
74
  background-color: transparent !important;
75
  border: none !important;
76
- box-shadow: none !important;
77
- max-width: 800px !important;
78
- margin: 0 auto !important;
79
- width: 100% !important;
80
- flex-grow: 1 !important; /* Заставляем чат занять все свободное место над плашкой */
81
- min-height: 0 !important; /* КРИТИЧЕСКИ ВАЖНО: убивает бесконечный цикл */
82
  }
83
 
84
- #chatbot > .wrapper {
85
- background: transparent !important;
86
- border: none !important;
87
- min-height: 0 !important;
88
- }
89
-
90
- /* 5. Настройки пузырей сообщений и текста */
91
- #chatbot .message,
92
- #chatbot [data-testid="user"],
93
- #chatbot [data-testid="bot"],
94
- #chatbot .message p,
95
- #chatbot .message span,
96
- #chatbot .message li,
97
- #chatbot .message ul,
98
- #chatbot .message ol {
99
- font-size: 0.98rem !important;
100
- line-height: 1.6 !important;
101
- }
102
-
103
- /* Сообщения пользователя */
104
- #chatbot .user, #chatbot [data-testid="user"] {
105
  background-color: #2f2f2f !important;
106
- color: #ffffff !important;
107
  border: 1px solid #3c3c3c !important;
108
- border-radius: 20px !important;
109
- border-bottom-right-radius: 4px !important;
110
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
111
- margin-left: auto !important;
112
- max-width: 80% !important;
113
- }
114
-
115
- #chatbot .user *, #chatbot [data-testid="user"] * {
116
  color: #ffffff !important;
117
  }
118
 
119
- /* Сообщения бота */
120
- #chatbot .bot, #chatbot [data-testid="bot"] {
121
  background-color: transparent !important;
122
  color: #ececec !important;
123
- margin-right: auto !important;
124
- max-width: 100% !important;
125
- padding-left: 0 !important;
126
- }
127
-
128
- #chatbot .bot *, #chatbot [data-testid="bot"] * {
129
- color: #ececec !important;
130
- }
131
-
132
- /* 6. Поле ввода (ChatGPT стиль) */
133
- #input-area {
134
- max-width: 800px !important;
135
- width: 100% !important;
136
- margin: 0 auto 1.5rem auto !important;
137
- padding: 0 1rem !important;
138
- background: transparent !important;
139
- border: none !important;
140
- flex-shrink: 0 !important; /* КРИТИЧЕСКИ ВАЖНО: запрещает чату сжимать и выталкивать плашку */
141
- }
142
-
143
- #input-area > div,
144
- #input-area .form,
145
- #input-area .gr-box {
146
- background-color: transparent !important;
147
- border: none !important;
148
- box-shadow: none !important;
149
  }
150
 
151
- #input-area textarea {
152
- border-radius: 26px !important;
153
- border: 1px solid #424242 !important;
154
  background-color: #2f2f2f !important;
 
 
155
  color: #ffffff !important;
156
- padding: 14px 20px !important;
157
- font-size: 1rem !important;
158
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25) !important;
159
  }
160
-
161
- #input-area textarea:focus {
162
  border-color: #676767 !important;
163
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35) !important;
164
  }
165
 
166
  /* Кнопка отправки */
167
- #input-area button,
168
- #input-area .gr-button-primary {
169
- border-radius: 22px !important;
170
  background-color: #ececec !important;
171
  color: #171717 !important;
172
- font-weight: 600 !important;
173
- border: none !important;
174
- padding: 0 20px !important;
175
- height: 44px !important;
176
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
177
  }
178
-
179
- #input-area button:hover,
180
- #input-area .gr-button-primary:hover {
181
  background-color: #ffffff !important;
182
  }
183
 
184
- /* Тонкий скроллбар */
185
- ::-webkit-scrollbar {
186
- width: 6px;
187
- height: 6px;
188
- }
189
- ::-webkit-scrollbar-track {
190
- background: transparent;
191
- }
192
- ::-webkit-scrollbar-thumb {
193
- background: #424242;
194
- border-radius: 10px;
195
- }
196
- ::-webkit-scrollbar-thumb:hover {
197
- background: #4f4f4f;
198
- }
199
-
200
- /* Скрытие дублирующих элементов */
201
- .stale {
202
- opacity: 1 !important;
203
- filter: none !important;
204
- }
205
- footer {
206
- display: none !important;
207
- }
208
  """
 
1
+ # src/ui/styles.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ CSS = """
4
+ /* Темная тема фона */
5
+ body, .gradio-container {
6
  background-color: #212121 !important;
 
 
 
 
 
 
7
  }
8
 
9
+ /* Прозрачный фон самого чата */
10
  #chatbot {
11
  background-color: transparent !important;
12
  border: none !important;
 
 
 
 
 
 
13
  }
14
 
15
+ /* Пузырь пользователя (справа, с рамкой) */
16
+ .user {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  background-color: #2f2f2f !important;
 
18
  border: 1px solid #3c3c3c !important;
19
+ border-radius: 20px 20px 4px 20px !important;
 
 
 
 
 
 
 
20
  color: #ffffff !important;
21
  }
22
 
23
+ /* Текст бота (без фона, как в ChatGPT) */
24
+ .bot {
25
  background-color: transparent !important;
26
  color: #ececec !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
+ /* Поле ввода */
30
+ textarea {
 
31
  background-color: #2f2f2f !important;
32
+ border: 1px solid #424242 !important;
33
+ border-radius: 24px !important;
34
  color: #ffffff !important;
35
+ padding: 12px 18px !important;
 
 
36
  }
37
+ textarea:focus {
 
38
  border-color: #676767 !important;
 
39
  }
40
 
41
  /* Кнопка отправки */
42
+ .gr-button-primary {
43
+ border-radius: 24px !important;
 
44
  background-color: #ececec !important;
45
  color: #171717 !important;
46
+ font-weight: bold !important;
 
 
 
 
47
  }
48
+ .gr-button-primary:hover {
 
 
49
  background-color: #ffffff !important;
50
  }
51
 
52
+ /* Скрываем подвал с логотипом Gradio */
53
+ footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  """