AnatoliiG commited on
Commit
106cde4
·
1 Parent(s): 2b63295

chatgpt ui

Browse files
Files changed (2) hide show
  1. src/ui/callbacks.py +163 -113
  2. src/ui/styles.py +122 -61
src/ui/callbacks.py CHANGED
@@ -1,18 +1,18 @@
1
- import json
2
- import uuid
 
3
 
4
  import gradio as gr
 
5
 
6
  from src.core.engine import engine
7
- from src.utils.helpers import (
8
- extract_text_from_file,
9
- get_clean_text,
10
- get_tools_schema,
11
- web_search,
12
- )
13
 
14
 
15
  def user_input(user_message, history):
 
 
 
16
  if not user_message:
17
  return None, history
18
  history = history or []
@@ -21,128 +21,178 @@ def user_input(user_message, history):
21
 
22
 
23
  def set_interactive(is_interactive):
 
 
 
24
  return (
25
  gr.update(
26
  interactive=is_interactive,
27
- placeholder="Wait..."
28
  if not is_interactive
29
- else "Type your message here...",
30
  ),
31
  gr.update(interactive=is_interactive),
32
  )
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def bot_response(
36
  history, system_prompt, temperature, max_tokens, use_search, uploaded_file
37
  ):
38
- # 1. Читаем файл, если он загружен
39
- file_context = ""
40
- if uploaded_file is not None:
41
- file_text = extract_text_from_file(uploaded_file)
42
- # Ограничиваем длину файла, чтобы не выбить OOM (Out Of Memory)
43
- file_context = f"\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_text[:8000]}\n"
44
-
45
- messages = [{"role": "system", "content": system_prompt + file_context}]
46
- for msg in history[-7:]:
47
- if msg["role"] in ["user", "assistant"]:
48
- messages.append(
49
- {"role": msg["role"], "content": get_clean_text(msg["content"])}
50
- )
51
-
52
- history.append({"role": "assistant", "content": ""})
53
- tools = get_tools_schema() if use_search else None
54
 
55
- # Цикл Агента (максимум 3 шага, чтобы не зависал вечно)
56
- max_iterations = 3
57
- for step in range(max_iterations):
58
  try:
59
- stream = engine.generate(
60
- messages=messages,
61
- max_tokens=max_tokens,
62
- temperature=temperature,
63
- stream=True,
64
- tools=tools,
65
- )
 
66
 
67
- partial_text = ""
68
- tool_call_name = ""
69
- tool_call_args = ""
70
- is_tool_call = False
71
-
72
- # Читаем стрим токенов
73
- for chunk in stream:
74
- delta = chunk["choices"][0].get("delta", {})
75
-
76
- # 1. Если модель пишет обычный текст
77
- if "content" in delta and delta["content"]:
78
- partial_text += delta["content"]
79
- # Обновляем UI без перезаписи истории инструментов
80
- history[-1]["content"] = partial_text
81
- yield history
82
-
83
- # 2. Если модель решила вызвать инструмент (поиск)
84
- if "tool_calls" in delta and delta["tool_calls"]:
85
- is_tool_call = True
86
- tc = delta["tool_calls"][0]
87
- if "function" in tc:
88
- if "name" in tc["function"]:
89
- tool_call_name += tc["function"]["name"]
90
- if "arguments" in tc["function"]:
91
- tool_call_args += tc["function"]["arguments"]
92
-
93
- # Если модель НЕ вызывала инструменты - завершаем цикл (Ответ готов!)
94
- if not is_tool_call:
95
  break
96
 
97
- # === ВЫПОЛНЕНИЕ ИНСТРУМЕНТА ===
98
- call_id = f"call_{uuid.uuid4().hex[:8]}"
99
- history[-1]["content"] += (
100
- f"\n\n*( 🌐 Ищу в интернете: `{tool_call_args}`... )*\n"
 
101
  )
102
  yield history
103
 
104
- try:
105
- args_dict = json.loads(tool_call_args)
106
- if tool_call_name == "web_search":
107
- tool_result = web_search(args_dict.get("query", ""))
108
- else:
109
- tool_result = f"Unknown tool: {tool_call_name}"
110
- except Exception as e:
111
- tool_result = f"Error executing tool: {e}"
112
-
113
- # Сохраняем вызов функции и её результат в историю для llama-cpp
114
- messages.append(
115
- {
116
- "role": "assistant",
117
- "content": partial_text,
118
- "tool_calls": [
119
- {
120
- "id": call_id,
121
- "type": "function",
122
- "function": {
123
- "name": tool_call_name,
124
- "arguments": tool_call_args,
125
- },
126
- }
127
- ],
128
- }
129
- )
130
- messages.append(
131
- {
132
- "role": "tool",
133
- "tool_call_id": call_id,
134
- "name": tool_call_name,
135
- "content": str(tool_result),
136
- }
137
- )
138
-
139
- # Очищаем ответ в чате для фин��льного вывода
140
- history[-1]["content"] += f"*( ✅ Найдено! Анализирую... )*\n\n"
141
- yield history
142
-
143
- # Идём на следующий круг `for step in range...`, чтобы модель прочитала результат поиска и ответила!
144
-
145
- except Exception as e:
146
- history[-1]["content"] += f"\n\n❌ Ошибка генерации: {str(e)}"
147
- yield history
148
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import urllib.parse
4
 
5
  import gradio as gr
6
+ import httpx
7
 
8
  from src.core.engine import engine
9
+ 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
 
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(),
77
+ "url": raw_url,
78
+ "snippet": snippet_clean,
79
+ }
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
  )
139
  yield history
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,
180
+ max_tokens=max_tokens,
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
src/ui/styles.py CHANGED
@@ -2,9 +2,12 @@ CSS = """
2
  html, body, .gradio-container {
3
  height: 100% !important;
4
  margin: 0 !important;
 
 
 
5
  }
6
 
7
- /* 1. Глобальный контейнер: занимаем все пространство без прокрутки страницы */
8
  .gradio-container {
9
  max-width: 100% !important;
10
  margin: 0 !important;
@@ -12,137 +15,195 @@ html, body, .gradio-container {
12
  height: 100vh !important;
13
  display: flex !important;
14
  flex-direction: column !important;
15
- background-color: var(--background-fill-primary) !important;
16
  overflow: hidden !important;
 
17
  }
18
 
19
- /* 2. Основной ряд: Sidebar + Chat
20
- Сделано flex: main-row занимает оставшееся пространство и не вызывает лишнюю высоту */
21
  #main-row {
22
  display: flex !important;
23
  flex: 1 1 auto !important;
24
  height: 100% !important;
25
  margin: 0 !important;
26
  gap: 0 !important;
 
27
  overflow: hidden !important;
28
  }
29
 
30
- /* 3. Сайдбар: фиксированная ширина, скролл при переполнении */
31
  #sidebar-container {
32
- width: 320px !important;
33
- min-width: 240px !important;
34
- max-width: 380px !important;
35
- border-right: 1px solid var(--border-color-primary) !important;
36
- background-color: var(--background-fill-secondary) !important;
37
- padding: 1.25rem !important;
38
  height: 100% !important;
39
  display: flex !important;
40
  flex-direction: column !important;
41
- box-shadow: 2px 0 8px rgba(0, 0, 0, 0.02) !important;
42
- overflow: auto !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
- /* Убираем "старение" (побледнение) при обновлении данных */
 
 
 
 
 
 
 
 
46
  .stale {
47
  opacity: 1 !important;
48
  filter: none !important;
49
  }
50
 
51
- /* 5. Правая колонка с чатом (Главная область) */
52
  #col-chat-main {
53
  display: flex !important;
54
  flex-direction: column !important;
55
  height: 100% !important;
56
  padding: 0 !important;
57
  margin: 0 !important;
58
- gap: 0 !important;
59
- background: var(--background-fill-primary) !important;
60
  flex: 1 1 auto !important;
61
  overflow: hidden !important;
 
62
  }
63
 
64
- /* ЧАТБОТ: Растягиваем на всю доступную высоту, min-height:0 для корректного flex-scroll */
65
  #chatbot {
66
  flex: 1 1 auto !important;
67
- height: auto !important;
68
- min-height: 0 !important;
 
 
69
  border: none !important;
70
- border-radius: 0 !important;
71
- margin: 0 !important;
72
- display: flex !important;
73
- flex-direction: column !important;
74
  overflow-y: auto !important;
75
  }
76
 
77
- /* Фикс для внутренней обертки Gradio Chatbot - предотвращаем overflow issues */
78
  #chatbot > .wrapper {
79
  flex: 1 1 auto !important;
80
  display: flex !important;
81
  flex-direction: column !important;
82
- min-height: 0 !important;
83
- overflow: auto !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
 
86
- /* 6. Поле ввода: фиксируем снизу с помощью sticky, чтобы всегда быть видимым */
87
  #input-area {
88
- padding: 0.75rem 1rem !important;
89
- border-top: 1px solid var(--border-color-primary) !important;
90
- background: var(--background-fill-primary) !important;
91
- box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.02) !important;
 
 
 
92
  flex-shrink: 0 !important;
93
- position: sticky !important;
94
- bottom: 0 !important;
95
- z-index: 10 !important;
96
  }
97
 
98
- /* Текстовое поле адаптируется по ширине и не выходит за границы */
99
  #input-area textarea {
100
- border-radius: 10px !important;
101
- border: 1px solid var(--border-color-primary) !important;
102
- padding: 12px 16px !important;
 
 
103
  font-size: 1rem !important;
104
- transition: border-color 0.2s ease !important;
105
- width: 100% !important;
106
- box-sizing: border-box !important;
107
  }
108
 
109
  #input-area textarea:focus {
110
- border-color: var(--color-accent) !important;
111
- box-shadow: 0 0 0 2px rgba(var(--color-accent-rgb), 0.1) !important;
112
  }
113
 
114
- /* 7. Кнопки и прочие элементы */
115
- .gr-button-primary {
116
- border-radius: 8px !important;
 
 
117
  font-weight: 600 !important;
 
 
 
118
  }
119
 
120
- /* Кастомный скроллбар */
121
- ::-webkit-scrollbar {
122
- width: 8px;
123
- height: 8px;
124
  }
125
 
 
 
 
 
 
126
  ::-webkit-scrollbar-track {
127
  background: transparent;
128
  }
129
-
130
  ::-webkit-scrollbar-thumb {
131
- background: var(--neutral-300);
132
  border-radius: 10px;
133
  }
134
-
135
  ::-webkit-scrollbar-thumb:hover {
136
- background: var(--neutral-400);
137
  }
138
 
139
- /* Скрытие футера */
140
  footer {
141
  display: none !important;
142
  }
143
-
144
- /* Убираем лишние зазоры между элементами */
145
- .gap {
146
- gap: 12px !important;
147
- }
148
  """
 
2
  html, body, .gradio-container {
3
  height: 100% !important;
4
  margin: 0 !important;
5
+ background-color: #212121 !important; /* Фоновый цвет ChatGPT */
6
+ color: #ececec !important;
7
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
8
  }
9
 
10
+ /* 1. Глобальный контейнер */
11
  .gradio-container {
12
  max-width: 100% !important;
13
  margin: 0 !important;
 
15
  height: 100vh !important;
16
  display: flex !important;
17
  flex-direction: column !important;
 
18
  overflow: hidden !important;
19
+ background-color: #212121 !important;
20
  }
21
 
22
+ /* 2. Основная рабочая область */
 
23
  #main-row {
24
  display: flex !important;
25
  flex: 1 1 auto !important;
26
  height: 100% !important;
27
  margin: 0 !important;
28
  gap: 0 !important;
29
+ background-color: #212121 !important;
30
  overflow: hidden !important;
31
  }
32
 
33
+ /* 3. Боковая панель в стиле ChatGPT */
34
  #sidebar-container {
35
+ width: 280px !important;
36
+ min-width: 280px !important;
37
+ max-width: 280px !important;
38
+ background-color: #171717 !important; /* Более темный левый сайдбар */
39
+ border-right: 1px solid #2f2f2f !important;
40
+ padding: 1.5rem 1.25rem !important;
41
  height: 100% !important;
42
  display: flex !important;
43
  flex-direction: column !important;
44
+ overflow-y: auto !important;
45
+ }
46
+
47
+ /* Заголовки и тексты на сайдбаре */
48
+ #sidebar-container h2, #sidebar-container h3, #sidebar-container span {
49
+ color: #ececec !important;
50
+ font-weight: 600 !important;
51
+ }
52
+
53
+ /* Контейнеры на боковой панели */
54
+ #sidebar-container .gr-form, #sidebar-container .gr-box {
55
+ background-color: #212121 !important;
56
+ border: 1px solid #2f2f2f !important;
57
+ border-radius: 14px !important;
58
+ }
59
+
60
+ /* Кнопки на боковой панели */
61
+ #sidebar-container button {
62
+ border-radius: 20px !important;
63
+ font-weight: 500 !important;
64
+ transition: all 0.2s ease !important;
65
  }
66
 
67
+ #sidebar-container .gr-button-stop {
68
+ background-color: #442a2a !important;
69
+ border: none !important;
70
+ }
71
+ #sidebar-container .gr-button-stop:hover {
72
+ background-color: #663a3a !important;
73
+ }
74
+
75
+ /* Убираем эффекты старения элементов Gradio */
76
  .stale {
77
  opacity: 1 !important;
78
  filter: none !important;
79
  }
80
 
81
+ /* 4. Колонка чата (Сбалансированная ширина и центрирование как на ChatGPT) */
82
  #col-chat-main {
83
  display: flex !important;
84
  flex-direction: column !important;
85
  height: 100% !important;
86
  padding: 0 !important;
87
  margin: 0 !important;
88
+ background-color: #212121 !important;
 
89
  flex: 1 1 auto !important;
90
  overflow: hidden !important;
91
+ align-items: center !important;
92
  }
93
 
94
+ /* Область самого чата */
95
  #chatbot {
96
  flex: 1 1 auto !important;
97
+ width: 100% !important;
98
+ max-width: 800px !important; /* Центрирует и ограничивает контент */
99
+ margin: 0 auto !important;
100
+ background: transparent !important;
101
  border: none !important;
102
+ box-shadow: none !important;
 
 
 
103
  overflow-y: auto !important;
104
  }
105
 
106
+ /* Внутренняя обертка чата */
107
  #chatbot > .wrapper {
108
  flex: 1 1 auto !important;
109
  display: flex !important;
110
  flex-direction: column !important;
111
+ background: transparent !important;
112
+ border: none !important;
113
+ }
114
+
115
+ /* Закругление пузырей сообщений */
116
+ #chatbot .message, #chatbot [data-testid="user"], #chatbot [data-testid="bot"] {
117
+ border-radius: 20px !important;
118
+ padding: 12px 18px !important;
119
+ max-width: 80% !important;
120
+ margin-bottom: 1rem !important;
121
+ line-height: 1.6 !important;
122
+ font-size: 0.98rem !important;
123
+ }
124
+
125
+ /* Сообщения пользователя: скругленная аккуратная плашка справа */
126
+ #chatbot .user, #chatbot [data-testid="user"] {
127
+ background-color: #2f2f2f !important;
128
+ color: #f9f9f9 !important;
129
+ border-bottom-right-radius: 4px !important;
130
+ align-self: flex-end !important;
131
+ margin-left: auto !important;
132
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05) !important;
133
+ }
134
+
135
+ /* Сообщения бота: плоский минималистичный стиль на чистом фоне слева */
136
+ #chatbot .bot, #chatbot [data-testid="bot"] {
137
+ background-color: transparent !important;
138
+ color: #ececec !important;
139
+ align-self: flex-start !important;
140
+ margin-right: auto !important;
141
+ padding-left: 0 !important;
142
+ max-width: 100% !important;
143
  }
144
 
145
+ /* 5. Поле ввода кругленное, парящее внизу экрана) */
146
  #input-area {
147
+ width: 100% !important;
148
+ max-width: 800px !important;
149
+ margin: 0 auto 2rem auto !important;
150
+ padding: 0 1rem !important;
151
+ background: transparent !important;
152
+ border: none !important;
153
+ box-shadow: none !important;
154
  flex-shrink: 0 !important;
 
 
 
155
  }
156
 
 
157
  #input-area textarea {
158
+ border-radius: 26px !important;
159
+ border: 1px solid #424242 !important;
160
+ background-color: #2f2f2f !important;
161
+ color: #ececec !important;
162
+ padding: 14px 20px !important;
163
  font-size: 1rem !important;
164
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
165
+ transition: border-color 0.2s, box-shadow 0.2s !important;
166
+ resize: none !important;
167
  }
168
 
169
  #input-area textarea:focus {
170
+ border-color: #676767 !important;
171
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3) !important;
172
  }
173
 
174
+ /* Кнопка отправки */
175
+ #input-area .gr-button-primary {
176
+ border-radius: 22px !important;
177
+ background-color: #ececec !important;
178
+ color: #171717 !important;
179
  font-weight: 600 !important;
180
+ border: none !important;
181
+ padding: 8px 16px !important;
182
+ transition: background-color 0.15s ease !important;
183
  }
184
 
185
+ #input-area .gr-button-primary:hover {
186
+ background-color: #d1d1d1 !important;
 
 
187
  }
188
 
189
+ /* Тонкий красивый скроллбар */
190
+ ::-webkit-scrollbar {
191
+ width: 6px;
192
+ height: 6px;
193
+ }
194
  ::-webkit-scrollbar-track {
195
  background: transparent;
196
  }
 
197
  ::-webkit-scrollbar-thumb {
198
+ background: #424242;
199
  border-radius: 10px;
200
  }
 
201
  ::-webkit-scrollbar-thumb:hover {
202
+ background: #4f4f4f;
203
  }
204
 
205
+ /* Скрываем стандартный футер Gradio */
206
  footer {
207
  display: none !important;
208
  }
 
 
 
 
 
209
  """