Макеты — один sketches/index.html; approved/ удалён

- sketches/index.html: все экраны одним документом в порядке 1·… (14–19 — бывшие approved)
- approved/ (4 файла групп + 3 макета + README) удалён как дубль
- стили вынесены в sketches/style.css
- README, THEMES, BORROW-FROM-ASSISTENT, MARKDOWN-SOURCE, MIC-ASR-SEARCH
  приведены к фактическому состоянию
This commit is contained in:
2026-09-28 10:07:57 +03:00
parent 960cd60120
commit 27f349550c
25 changed files with 2416 additions and 4448 deletions
+4 -2
View File
@@ -78,7 +78,9 @@ compose.desktop {
## 2. Отрисовка Markdown
Отдельный документ: **`MARKDOWN-SOURCE.md`** — 4 файла, разобрано подробно.
**Уже скопировано в проект** (28.09.2026): 6 файлов + 11 тестов парсера, пакеты
переименованы под `pw.binom.agentik.desktop.*`. Подробности, карта «источник →
назначение», зависимости и как пересинхронизировать — в **`MARKDOWN-SOURCE.md`**.
## 3. Приёмы отрисовки (самое ценное)
@@ -343,7 +345,7 @@ val DarkClientTheme = ClientTheme(
| Берём | Не берём |
|---|---|
| Версии и плагины, репозитории | Архитектуру и слои |
| Отрисовку Markdown (4 файла) | Экраны целиком (`ChatScreen`, `SettingsDialog`, …) |
| Отрисовку Markdown (6 файлов, уже скопированы) | Экраны целиком (`ChatScreen`, `SettingsDialog`, …) |
| Приёмы: таблицы, ленивый список, автоскролл, полоса прокрутки | Модули `chat-*` и модели сообщений |
| Кликабельные ссылки, копирование кода, выделение текста | `MessageBubble` |
| Приём записи звука (формат 16к/моно, `javax.sound`) | `ChatViewModel`, `config/*`, `cache/*` |
+98 -112
View File
@@ -1,150 +1,136 @@
# Откуда брать отрисовку Markdown
# Отрисовка Markdown: откуда взято и где лежит
Готовый рендер Markdown для Compose уже написан и лежит в репозитории **`ai/assistent`**.
Писать своё не надо — надо взять это.
**Статус: УЖЕ СКОПИРОВАНО В ПРОЕКТ (28.09.2026).** Код вендорен в `src/`,
собирается, тесты парсера (11 шт.) зелёные. Своё писать не надо.
**Репозиторий:** https://git.binom.pw/ai/assistent (ветка `main`)
**Проверено на коммите:** `2c5b3f9` (файлы добавлены в `1acced4`, 2026-08-13)
**Важно:** репозиторий приватный. Браузерная ссылка откроется только у залогиненного;
для копирования файлов из скрипта — RAW-адреса с логином/паролем (ниже).
Источник — приватный репозиторий **`ai/assistent`** (https://git.binom.pw/ai/assistent,
ветка `main`, проверено на коммите `2c5b3f9`, файлы добавлены в `1acced4` 2026-08-13).
Пакеты переименованы под наш проект; внешняя зависимость — одна
(`ru.otpbank.ai:markdown`).
---
## Что именно копировать: 4 файла
## Куда что легло
Всё лежит в двух модулях. Модуль `client-shared` — общий, собирается под **JVM + Compose**
(то есть ровно наш случай, десктоп), не под Android.
| В assistent | У нас |
|---|---|
| `client-shared/.../shared/markdown/MarkdownSegment.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/markdown/MarkdownSegment.kt` |
| `client-shared/.../shared/markdown/MarkdownParserImpl.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/markdown/MarkdownParserImpl.kt` |
| `client-shared/.../shared/ui/MarkdownRendering.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt` |
| `client-shared/.../shared/ui/ChatImage.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt` |
| `client-shared/.../shared/ClientImageSupport.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ClientImageSupport.kt` |
| `client/.../client/ui/MarkdownRenderer.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRenderer.kt` |
| `client-shared/src/test/.../markdown/MarkdownParserTest.kt` | `src/jvmTest/kotlin/pw/binom/agentik/desktop/markdown/MarkdownParserTest.kt` |
### 1. Модель данных — `MarkdownSegment.kt` (59 строк)
Переименование пакетов:
```
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt
```
- `pw.binom.client.shared.markdown` → `pw.binom.agentik.desktop.markdown`
- `pw.binom.client.shared.ui` → `pw.binom.agentik.desktop.ui`
- `pw.binom.client.shared` (`ClientImageSupport`) → `pw.binom.agentik.desktop.ui`
- `pw.binom.client.ui` (`MarkdownRenderer`) → `pw.binom.agentik.desktop.ui`
Что внутри: типы кусков разметки — текст, блок кода, таблица, картинка, разделитель.
Плюс описание стилевого участка: жирный, курсив, код, зачёркнутый, ссылка.
Зависимостей от Compose нет вообще — чистые данные.
### Почему файлов шесть, а не четыре
### 2. Парсер — `MarkdownParserImpl.kt` (353 строки)
Изначально в доке значились 4 файла. При сборке выяснилось, что
`MarkdownRendering.kt` для сегментов-картинок вызывает `ChatImage(...)`, а тот —
`ClientImageSupport`. Поэтому взяты ещё `ChatImage.kt` и `ClientImageSupport.kt`.
Оба независимы от `chat-*` и `MessageBubble`.
```
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt
```
Точка входа: `parseMarkdown(markdown: String): List<MarkdownSegment>`.
Единственная внешняя зависимость — библиотека парсера `ru.otpbank.ai:markdown` (см. ниже).
От Композа не зависит.
### 3. Отрисовка — `MarkdownRendering.kt` (377 строк)
```
client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt
```
Точка входа:
`renderMarkdownSegments(content, textColor, fontSize, isBubbleScrolled, onUrlClick)`.
Внутри: текст с inline-стилями, кликабельные ссылки, блоки кода с копированием,
таблицы, картинки, разделители. Самый жирный файл и самая нужная часть.
### 4. Десктопная обёртка — `MarkdownRenderer.kt` (25 строк)
```
client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt
```
Открытие ссылок в системном браузере (`Desktop.getDesktop().browse`) и
`parseMultiline`. Модуль `client` в assistent — это **десктопный** клиент на
Compose Desktop, так что пример использования ровно наш.
---
## Что копировать НЕ надо
- `client-shared/src/main/kotlin/pw/binom/client/shared/ui/MessageBubble.kt` (789 строк) —
пузырь сообщения целиком. Тянет за собой модуль `chat-common` (модели сообщений),
из плюшек — выделение текста и контекстное меню. Для отрисовки разметки не нужен,
но полезен, если понадобится пример.
**Границы заимствования — подробнее в `BORROW-FROM-ASSISTENT.md`.** Коротко:
из assistent берём приёмы отрисовки, плагины и версии; архитектуру, слои и готовые
экраны (`ChatScreen`, `SettingsDialog`, модули `chat-*`) — не берём, у нас своё.
---
## Зависимости
Одна, и она есть в нашем Nexus:
`ClientImageSupport` — это реестр: платформенный клиент при старте регистрирует
`decoder` (байты → `ImageBitmap`) и, при желании, `fetcher` и `serverBaseUrl`.
Пока `decoder` не зарегистрирован, картинки деградируют в текст (alt/ссылку) —
ничего не падает. На десктопе декодер регистрируется так:
```kotlin
implementation("ru.otpbank.ai:markdown:0.51.0")
```
Репозиторий в `build.gradle.kts` (без него не соберётся):
```kotlin
maven {
url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/")
isAllowInsecureProtocol = true
ClientImageSupport.decoder = { bytes ->
org.jetbrains.skia.Image.makeFromEncoded(bytes).toComposeImageBitmap()
}
```
Проверено: в Nexus лежит, версии с `0.1.0` по `0.51.0`, файл `markdown-0.51.0.pom`
отдаёт `HTTP 200`.
## Точки входа
Из Compose нужны: `compose.material3`, `compose.foundation`, `compose.ui`,
`compose.materialIconsExtended` (иконка копирования в блоках кода).
- Парсинг: `parseMarkdown(markdown: String): List<MarkdownSegment>` (в `...desktop.markdown`).
- Отрисовка: `renderMarkdownSegments(content, textColor, fontSize, isBubbleScrolled, onUrlClick)` (в `...desktop.ui`).
- Открытие ссылок/`parseMultiline`: `...desktop.ui.MarkdownRenderer`.
## Опубликован ли `client-shared` как библиотека
## Зависимости и сборка (уже настроено)
**Нет.** Проверено поиском в Nexus (`pw/binom/client-shared`, `pw/binom/chat-common`) —
`HTTP 404`. Поэтому вариантов два: либо копируем исходники к себе (4 файла выше),
либо сперва публикуем модуль. Копирование проще.
`gradle/libs.versions.toml`:
---
```toml
compose-multiplatform = "1.7.3" # версия из assistent; собирается на нашем Kotlin 2.4.20
markdown = "0.51.0"
## Как копировать (RAW-адреса, работают с логином)
markdown = { module = "ru.otpbank.ai:markdown", version.ref = "markdown" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
compose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
```
Адрес вида `/raw/branch/main/<путь>` отдаёт файл. Проверено: `HTTP 200`, 1805 байт —
совпадает с локальным размером.
`build.gradle.kts` — плагины `kotlin-compose` + `compose`, зависимости
`compose.foundation` / `compose.ui` / `compose.material3` /
`compose.materialIconsExtended` (иконка копирования) + `libs.markdown`.
`compose.desktop.currentOs` пока НЕ подключён — он понадобится, когда появится
настоящее окно (`compose.desktop { application { mainClass = ... } }`), для
компиляции рендера он не нужен.
`settings.gradle.kts` — репозитории (без них `ru.otpbank.ai:markdown` не найдётся):
```kotlin
maven {
name = "developerspace"
url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/")
setAllowInsecureProtocol(true)
}
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://maven.google.com")
```
Проверено: `./gradlew compileKotlinJvm` — OK; `./gradlew jvmTest` —
`MarkdownParserTest` 11/11, `ConversationMetaRepositoryIT` 14/14.
## Как пересинхронизировать, если в assistent что-то поправят
RAW-адрес: `/raw/branch/main/<путь>` (нужен логин, репозиторий приватный).
```bash
BASE=https://git.binom.pw/ai/assistent/raw/branch/main
AUTH='subochev:ПАРОЛЬ' # подставить перед запуском
AUTH='subochev:ПАРОЛЬ'
for f in \
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt \
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt \
client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt \
client-shared/src/main/kotlin/pw/binom/client/shared/ui/ChatImage.kt \
client-shared/src/main/kotlin/pw/binom/client/shared/ClientImageSupport.kt \
client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt ; do
curl -sS -u "$AUTH" "$BASE/$f" -o "$(basename "$f")"
curl -sS -u "$AUTH" "$BASE/$f" -o "/tmp/$(basename "$f")"
done
```
Браузерные ссылки для чтения глазами (нужен залогиненный git.binom.pw):
После копирования — тем же `sed`-ом переименовать пакеты (см. таблицу выше),
`MarkdownRenderer` дополнительно сослать `renderMarkdownSegments` на
`pw.binom.agentik.desktop.ui`.
```
https://git.binom.pw/ai/assistent/src/branch/main/client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt
https://git.binom.pw/ai/assistent/src/branch/main/client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt
https://git.binom.pw/ai/assistent/src/branch/main/client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt
https://git.binom.pw/ai/assistent/src/branch/main/client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt
```
---
## Что придётся поправить после копирования
1. **Пакеты.** Файлы лежат в `pw.binom.client.shared.*` — переименовать под наш проект
(или оставить как есть, если не смущает).
2. **`openUrl`.** В `MarkdownRenderer` открытие ссылок идёт через `java.awt.Desktop` —
на десктопе это работает, ничего менять не надо.
3. **Стиль.** Цвета текста и блоков кода в `MarkdownRendering` берутся из аргументов и
`MaterialTheme` — под нашу тёмную тему подставить свои значения.
## Что этот рендер умеет (по коду)
## Что рендер умеет (по коду)
- **Текст** с участками: жирный, курсив, код, зачёркнутый, ссылки.
- **Блоки кода** — с названием языка и кнопкой копирования.
- **Таблицы** — заголовки и строки, с горизонтальной прокруткой.
- **Картинки** — `data:`-ссылки и обычные `http(s)`. Первые рисуются сразу из памяти.
- **Блоки кода** — с названием языка и кнопкой копирования (`LocalClipboardManager`).
- **Таблицы** — заголовки/строки, переключаются на горизонтальный скролл по
`isBubbleScrolled` (иначе Compose падает на бесконечной maxWidth).
- **Картинки** — `data:` и `http(s)`; рисуются через `ChatImage`/`ClientImageSupport`.
- **Разделители** (`---`).
- **Кликабельные ссылки** — нажатие уходит в `onUrlClick`, оттуда открываем браузер.
- **Кликабельные ссылки** — нажатие уходит в `onUrlClick`.
## Известный баг на будущее
В assistent в `MessageBubble.kt:153` (НЕ скопирован) `hasTable` вызывает
`parseMarkdown` вторым проходом до основной отрисовки — двойной парс на пузырь.
У нас `MessageBubble` свой, при его написании в 0-й версии просто звать
`renderMarkdownSegments` один раз; кэшировать разбор через
`remember(record.id, record.text)`.
## Что НЕ брали
- `MessageBubble.kt` — тянет `chat-common` и чужие модели сообщений.
- Модули `chat-*`, `ChatScreen`, `ChatViewModel`, `SettingsDialog` и т. п.
- Границы заимствования — в `BORROW-FROM-ASSISTENT.md`.
+81
View File
@@ -1,5 +1,12 @@
# Микрофон и распознавание: результаты поиска
> **Статус: реализовано** (см. `src/jvmMain/.../desktop/voice/`).
> `mic-api` + `asr-vosk` + `vad` подключены в `gradle/libs.versions.toml`,
> кнопка микрофона в чате активна: `VoiceController` (микрофон → VAD →
> распознавание), `VoskVoiceRecognizer` (Vosk-модель и `silero_vad.onnx` лежат
> внутри артефактов — качать нечего). Ниже — исходный поиск, из которого это
> выросло.
**Задача была:** найти готовые библиотеки для работы с микрофоном и распознавания,
прежде чем брать код из `ai/assistent`. **Код не пишем** — только поиск.
@@ -172,3 +179,77 @@ client.post("$base/audio/transcriptions") { ... } // multipart, как у Open
| Нужны ли сторонние зависимости для звука? | **Нет.** Всё на стандартной Java (`javax.sound.sampled`) и sherpa-onnx для нейронок. |
**Код не писан** — только поиск, как и просили.
---
## 7. Что в итоге подключено
| Слой | Артефакт | Где в коде |
|---|---|---|
| Микрофон | `pw.binom.mic:mic-api:1.0.0` | `voice/VoiceController.kt` (`MicSource` → `MicAudio.record`) |
| Детектор речи | `pw.binom.voice.vad:vad:2` | `voice/VoiceRecognizer.kt` (`VadInstance`) |
| Распознавание | `pw.binom.asr:asr-vosk:4` (+ `asr-api:4`) | `voice/VoiceRecognizer.kt` (`VoskVoiceRecognizer`) |
| Сборка тракта | — | `voice/VoiceController.kt` (`VoiceState`: IDLE/PREPARING/RECORDING/RECOGNIZING) |
| Склейка с UI | — | `ui/AppState.kt` (`voiceState`, `voicePartial`, `composerDraft`), `ui/screens/ChatPane.kt` |
| Прогрев до skia | — | `voice/VoiceNative.kt` (`warmUp()` из `main()`) |
Тесты: `voice/AudioTest.kt` (s16le→float, нарезка окон VAD + реальная загрузка
`silero_vad.onnx`), `voice/VoiceControllerTest.kt` (фейковые микрофон и
распознаватель), `voice/VoskVoiceRecognizerIT.kt` (настоящие Vosk + VAD на
тишине), `voice/RealSpeechIT.kt` (настоящая русская речь после инициализации
skia — см. ниже).
---
## 8. ⚠️ Конфликт skiko и нативных библиотек (найдено 28.09.2026)
**Симптом.** Приложение Compose Desktop роняется с SIGSEGV (не исключение —
падение всего JVM) при первом нажатии микрофона. В `hs_err_pid`:
```
C [libc.so.6+0x184fdc]
C [libstdc++.so.6+0x15d820] std::ostream::_M_insert<double>
C [jnaNNNN.tmp+0x6838e7] kaldi::ParseOptions::RegisterSpecific(...)
j org.vosk.LibVosk.vosk_model_new(...)
```
То же самое — с ONNX (`OrtSession.createSession` → `std::ostream::_M_insert`):
C++-библиотека пишет в свой лог-стрим, а стрим оказывается невалидным.
**Причина — порядок загрузки.** skiko (нативный рендер Compose, грузит
`~/.skiko/<hash>/libskiko-linux-x64.so`) и C++-стек распознавания (Vosk/Kaldi,
onnxruntime) конфликтуют, если второй инициализируется ПОСЛЕ первого. Первое
создание модели/сессии после инициализации skia валит JVM.
**Проверено пробниками (позже удалены):**
| Порядок | Результат |
|---|---|
| ONNX → skia (raster) → ONNX | ок |
| skia (Compose-текст) → ONNX | **SIGSEGV** |
| skia (Compose-текст) → Vosk | **SIGSEGV** |
| **Vosk → skia (Compose-текст) → Vosk** | **ок** |
**Фикс.** `VoiceNative.warmUp()` вызывается в `main()` **до** `application {}`:
создаёт и закрывает `Stt.vosk(...)` и `VadInstance(...)`. После этого и свежий
распознаватель, и свежий VAD, созданные уже после отрисовки окна, работают.
Побочный плюс — модель Vosk (~46 МБ, ~2 с) грузится на старте, а не при первом
клике. Если прогрев не удался, `AppState.voiceAvailable = false` — кнопка
микрофона заблокирована (лучше «недоступно», чем краш).
**Следствие для тестов.** `jvmTest` форкает JVM на каждый тест-класс
(`forkEvery = 1`): иначе skiko-тесты (`ThemeRenderTest`) отравляют
нативные тесты голоса в том же JVM. `RealSpeechIT` проверяет ровно
продуктовый порядок: `warmUp()` → рендер Compose → создание распознавателя →
распознавание реального сэмпла (`src/jvmTest/resources/voice/raz-dva-tri.wav`
→ «раз два три четыре пять»).
Заметки на будущее:
- Создание распознавателя стоит секунды (загрузка модели Vosk), поэтому
распаковка zip в tmp кэшируется самим `asr-vosk`, а в UI есть состояние
«загружаю модель».
- `VadInstance.close()` **не идемпотентен** (второй `close()` роняет JVM через
ONNX `double free`) — закрывать ровно один раз.
+129 -107
View File
@@ -1,131 +1,153 @@
# agentik-desktop — дизайн десктопного клиента
# agentik-desktop — десктопный клиент агента agentik
Десктопный клиент агента **agentik** (Jetpack Compose Desktop). UI общается с агентом
через опубликованную библиотеку `pw.binom.agentik:client` — она даёт готовые
`Agent` / `Conversation`, живой поток событий и `interrupt()` для кнопки «Стоп».
Клиент агента **agentik** на Kotlin Multiplatform + **Compose Desktop** (цель сборки —
только JVM). UI общается с агентом через готовую библиотеку `pw.binom.agentik:client`:
она даёт `Agent` / `Conversation`, живой поток событий (SSE), авто-reconnect,
кэш списка диалогов и `interrupt()` для кнопки «Стоп».
Это пока **только дизайн**, не код приложения.
Проект уже **собирается и запускается** (`./gradlew run`), а не только спроектирован.
## Что где лежит
**`approved/` — принятое. Это решение, на него опираемся в реализации.**
```
approved/001-sidebar-utility.html # ОСНОВА: привычный мессенджер
approved/004-settings-agents.html # настройки агентов + проверка связи
approved/005-new-chat-picker.html # модалка «Новый диалог»
approved/006-groups-manage/ # группы: 4 файла по состояниям
approved/006-groups-manage/1-list.html # Группы: изменить, удалить, добавить
approved/006-groups-manage/2-add.html # новая группа
approved/006-groups-manage/3-edit.html # изменение и удаление
approved/006-groups-manage/4-delete.html # удаление: диалоги остаются
approved/README.md # что одобрено и когда
sketches/index.html # ВСЕ экраны одним документом: список диалогов, чат, запись,
# группы, агенты, проверка связи, пустые состояния.
# Это и черновик, и «одобренное» — отдельного approved/ больше нет.
sketches/style.css # палитра и стили макетов (цвета числами — так и надо, это макет)
src/jvmMain/kotlin/... # код приложения
src/jvmTest/kotlin/... # тесты
```
**`sketches/` — предложения. Не решение, а варианты и черновики.**
Документы рядом:
- `REQUIREMENTS.md` — требования (накиданы, ни один пункт не обязателен в текущей форме).
- `STORAGE.md` — что и где хранится на клиенте (SQLite + JSON).
- `CACHE.md` — кэш сообщений: что есть в библиотеке, схема «только новое».
- `THEMES.md` — цвета не пишем в коде, только из семантической схемы.
- `MARKDOWN-SOURCE.md` — откуда взята отрисовка Markdown.
- `BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent`, а что нет.
- `MIC-ASR-SEARCH.md` — библиотеки микрофона/распознавания (`mic-kmp`, `asr-kmp`, `vad-kmp`).
## Структура кода
```
sketches/002-rail-voice-first/index.html # голос — главное действие
sketches/003-three-pane-command/index.html # три панели + состояние агента
sketches/004-settings-agents/index.html # черновик настроек агентов
sketches/005-new-chat-picker/index.html # черновик модалки нового диалога
pw.binom.agentik.desktop
├── Main.kt # точка входа: окно, реестр, состояние
├── settings/ # AppPaths, AppSettings/AgentConfig, SettingsRepository
├── persistence/ # ConversationMetaRepository (группы/lastSeen/preview/watermark)
├── session/ # LiveTurn, ChatSession, AgentConnection, AgentRegistry
├── model/ # UiModels + Mappers (журнал/стрим → UI)
├── media/ # AvatarStorage (копия картинки агента)
├── net/ # ConnectionChecker (проверка связи)
├── markdown/ # вендоренный парсер Markdown (из assistent)
└── ui/ # App, AppState, theme/, components/, screens/
```
`BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent` (приёмы, плагины, версии),
а что не берём (архитектура, экраны, дизайн — своё).
`MIC-ASR-SEARCH.md` — библиотеки для микрофона и распознавания: своё готовое
(`mic-kmp`, `asr-kmp`, `vad-kmp`), вместо копирования кода из assistent.
`STORAGE.md` — где что хранится: **хранилище делаем абстракцией, реализация —
SQLite, настройки — JSON.** Три отдельные абстракции (сообщения / настройки /
снимок списка), почему SQLite, что менять в API (превью и число непрочитанных).
`CACHE.md` — кэш сообщений на клиенте: что для него есть в библиотеке, схема
загрузки «только новое», где может порваться.
`THEMES.md` — цветовые темы: цвета не пишем в коде, берём из схемы. Тем будет
несколько, поэтому ни одного цвета числом.
`MARKDOWN-SOURCE.md` — откуда брать готовую отрисовку Markdown (файлы и адреса).
`REQUIREMENTS.md` — требования. Статус: накидываем, ни один пункт не обязателен
к исполнению в том виде, как записан.
Ключевые решения по слоям — в `STORAGE.md`; кратко:
## Одобренное
- **Транспорт целиком в `:client`.** HTTP/JSON/SSE/Bearer, reconnect, кэш списка
диалогов — не переписываем.
- **Сообщения** — `KsqliteJournalStore` из `:journal-ksqlite` (offline-история).
- **Группы, `lastSeen`, превью, watermark синхронизации** — наш
`ConversationMetaRepository` (сервер про это не знает).
- **Отдельная SQLite-база на агента**: id диалогов серверные, у двух агентов
могут совпасть; плюс группы локальны для агента.
`approved/` — макеты, которые пользователь **принял**. Это решение, на него
опираемся в реализации. Отличие от `sketches/`: там предложения, здесь принятое.
## Текущее состояние реализации
- **`001-sidebar-utility.html`** — **основа дизайна**: привычный мессенджер,
список диалогов + чат, папки («Все чаты / Работа / Дом»). Выбран как основа.
- **`004-settings-agents.html`** — настройки агентов: список, форма нового,
проверка связи отдельным окном поверх. Одобрена 2026-09-19.
- **`005-new-chat-picker.html`** — модалка «Новый диалог» при нескольких
ассистентах. Одобрена 2026-09-19.
- **`006-groups-manage/`** — управление группами, четыре файла по состояниям.
Одобрена 2026-09-19. Пояснительной панели над окном в ней нет
(в `sketches/005` была — это была заметка для проверки, не часть интерфейса).
Готово и покрыто тестами:
**Правило:** одобренное живёт **только** в `approved/`. Если макет приняли —
он переезжает туда, а не остаётся «выбранным» среди вариантов.
- **Настройки** — `settings.json` (агенты, тема, выбранная группа, последний диалог),
`clientId` генерируется один раз. Атомарная запись.
- **Живой список диалогов** — `AgentConnection.conversationsFlow` опрашивает
встроенный кэш либы (`conversationStore.listFlow`, warmup-цикл на старте) и
дополнительно подписан на SSE-поток агента: диалог, созданный с другого
клиента (или самим сервером), появляется в списке сам; свои создания и
удаления отражаются сразу. Ошибки сервера не считаются «пустым списком» —
последний известный список сохраняется.
- **Реестр агентов** — добавление/правка/удаление, подключение, деградация в
«не отвечает» без падения приложения.
- **Стриминг** — `LiveTurn` применяет события `Working → StartReasoning →
AppendText → ToolCall/ToolResult → End/Interrupted/Error`; `ChatSession`
догоняет историю (`after = synced_at`), пишет в локальный кэш, склеивает
live + history, обновляет превью и `lastSeen`.
- **История/UI-модели** — мапперы `MessageRecord → UiMessage` со склейкой
`ToolCall`+`ToolResult` в одну карточку.
- **Группы** — CRUD групп + раскладка диалогов (каскад: удаление группы не
удаляет диалоги).
- **Тема** — семантические цвета (`AgentikTheme.colors`), тёмная + светлая
схемы; отдельно пользовательские цвета агентов.
- **Markdown** — вендоренный рендер из `ai/assistent` (см. `MARKDOWN-SOURCE.md`).
- **Аватары** — копирование выбранного файла в `~/.agentik-desktop/avatars/`.
- **Проверка связи** — «ответ сервера» + создание/удаление временного диалога,
с таймингами.
- **UI (Compose)** — рейка агентов, список диалогов (поиск, папки, бейджи),
чат с живым стримингом, композер, окна «Агенты», «Группы» и «Новый диалог»
(при нескольких агентах — выбор, при одном — сразу), пустые состояния,
адаптивная раскладка (узкое окно — список или чат).
- **Голос** — микрофон → VAD → распознавание (`voice/`): кнопка микрофона
или `Ctrl+M` из чата запускает запись (`mic-api`), Silero VAD (`vad-kmp`) отмечает наличие речи,
Vosk (`asr-vosk`, модель внутри артефакта) отдаёт промежуточный текст над
полем ввода, финал дописывается в черновик. Тишина текстом не становится.
Нативные модели считаются на одном выделенном потоке. На старте `main()`
делает [`VoiceNative.warmUp()`] — обязательный прогрев Vosk/ONNX ДО
инициализации skia/Compose (иначе создание модели после отрисовки окна
роняет JVM в C++-логах; подробности в `MIC-ASR-SEARCH.md`).
- **Реальная интеграция** — интеграционные тесты гоняются против живого
`:standalone`-агента (Qwen через vLLM), а не только in-memory-фейков:
`RealServerIT` (диалог → отправка → живой ответ LLM → локальный кэш) и
`RealAppStateIT` (весь `AppState`: реестр → подключение → живой список →
создание диалога → стриминг → превью/lastSeen), `RealSpeechIT` (речь:
прогрев нативных ASR → инициализация Compose → распознавание реального wav
→ «раз два три четыре пять»). См. `AGENTIK_E2E_URL`.
Проверено вживую (окно приложения + живой агент): список диалогов подтягивается
с сервера, кнопка «+» создаёт диалог, сообщение уходит и ответ приходит в UI,
`Ctrl+M` переводит микрофон в запись и обратно без падения JVM.
Не начато / заглушки:
- **Свёрнутый вид хода работы агента** — сейчас живой ход показывается
развёрнуто (reasoning, карточки ToolCall/ToolResult, спиннер). Как его
складывать — вопрос дизайна, не решён (REQUIREMENTS §9).
- **Кусок прерванного ответа в кэше** — R39 выполнен (прерванный ответ в
историю не пишется), а вот хранить ли его обрывок в кэше — вопрос не решён
(REQUIREMENTS §9).
- **Светлая тема визуально не проверялась глазами** (`THEMES.md`); в
`ui/ThemeRenderTest.kt` проверено программно: фон код-блоков берётся из
слота темы, порядок яркостей в светлой палитре корректный.
- **Голос: движок зашит на Vosk** (`VoskVoiceRecognizer`) — Qwen3/Whisper из
`asr-kmp` подключаются заменой `VoiceRecognizerFactory`; выбор движка/модели
в настройках не сделан.
## Как запустить
```bash
./gradlew run # окно приложения
./gradlew jvmTest # тесты (реальная SQLite, фейковый транспорт)
# Интеграционные тесты против живого агента (по умолчанию пропускаются):
AGENTIK_E2E_URL=http://127.0.0.1:8080/agentik ./gradlew jvmTest \
--tests '*RealServerIT' --tests '*RealAppStateIT'
./gradlew jvmJar # собрать jar
```
## Что решено
- **Сначала десктоп, затем Android — по тем же лекалам.** Десктопный клиент
делаем образцом: понятный, удобный и **читаемый по коду**. Когда получится —
Android повторяет те же решения. Поэтому цель — максимально простой код,
который пользователь открывает и понимает.
- **Основа — вариант 1** (`approved/001-sidebar-utility.html`). Остальные остаются рядом как
источник идей.
- Стиль — тёмный, из уже принятой темы клиента assistent: фон `#121218`,
панели `#17212B`, акцент `#6AB2F2`, текст `#EBEBEB`.
- **Тем будет несколько — цвета берём из цветовой схемы, не пишем числом в коде.**
Имена по смыслу («фон», «панель», «акцент»), чтобы смена темы ничего не ломала.
В макетах числа — это нормально. Разбор и список того, что забывают: `THEMES.md`.
- **Узкое окно** — на экране что-то одно: либо список диалогов, либо чат.
Переключение кнопкой «Назад», как в Телеграме. Широкое — обе части сразу.
- **Подсказка о горячих клавишах** под полем ввода убрана.
- **Счётчиков токенов и кольца заполнения не будет.** В библиотеке `client`
таких данных нет вообще: ни модели, ни отправлено/получено, ни предела
контекста. Правило: нет данных — нет цифры.
- **Несколько агентов** — предусмотреть. Различение: **цвет или картинка** на выбор.
Цвет помечает диалоги агента (полоска у строки, точка у группы); картинка
показывается кружком вместо цвета. **Картинку клиент копирует себе**, а не берёт
по исходному адресу — иначе значок пропадёт вместе с файлом.
- **Markdown** — показывать разметкой, а не сырыми символами. Готовый рендер
лежит в репозитории `ai/assistent` — писать свой не надо, брать оттуда.
Точные координаты файлов — в `MARKDOWN-SOURCE.md`.
- **Папки над списком диалогов** («Все чаты», «Работа», «Дом») — часть выбранного
варианта 1, не терять их при доработках.
- **Проверка связи с агентом** — в отдельном окне поверх настроек: идёт → отвечает
→ не отвечает, с подробностями и подсказкой. Добавить агента можно только
после успешной проверки.
- **Новый диалог** — при нескольких ассистентах спрашиваем, в каком создавать
(диалог живёт внутри ассистента, отдельно его не создать). Один ассистент —
окна нет, создаётся сразу. Ноль ассистентов — кнопки «+» нет вообще.
## Заимствования: только приёмы, не архитектура
Из репозитория `ai/assistent` берём **приёмы отрисовки, плагины и версии**.
Архитектуру, слои, готовые экраны и окна — **не берём**: дизайн у нас свой,
нарисованный (см. `sketches/`). Разбор по пунктам — в `BORROW-FROM-ASSISTENT.md`,
там же таблица «берём / не берём».
## Как смотреть
Открыть в браузере любой из файлов — каждый самодостаточный, без сборки.
В четвёртом сверху переключатель состояний: список агентов, форма, проверка
связи (успех и ошибка), список диалогов с двумя агентами.
- **Сначала десктоп, затем Android — по тем же лекалам.** Десктоп делаем
образцом: понятный и читаемый по коду. Потом Android повторяет решения.
- **Несколько агентов** — различение цветом или картинкой (выбирает пользователь).
Картинку клиент **копирует себе**, а не берёт по адресу.
- **Markdown** — показывать разметкой. Готовый рендер взят из `ai/assistent`.
- **Папки над списком диалогов** — сохранены.
- **Проверка связи** — отдельным окном, с таймингами шагов.
- **Ориентир по размеру:** ~9 небольших кусков логики поверх библиотеки, а не
собственная инфраструктура (см. `STORAGE.md`, `CACHE.md`).
## Что предстоит решить
1. **Запись** — по нажатию (нажал — говоришь — нажал) или на удержание.
1. **Запись** — по нажатию или на удержание.
2. Форма показа хода работы агента в свёрнутом виде.
3. Различение агентов: одного цвета может оказаться мало.
4. Где хранится список агентов.
5. Нужны ли папки для диалогов, как в мобильном клиенте, или хватит поиска.
6. **Где хранится кэш сообщений** — файл или лёгкая база (см. `CACHE.md`).
7. **Что делать с куском прерванного ответа** в кэше — иначе схема даст сбой
на первом же нажатии «Стоп» (`CACHE.md`, п. 3.2).
## Что сознательно не решается здесь
- **Откуда берётся распознавание речи.** Отдельная тема, к интерфейсу не
относится: в дизайне показано только место кнопки и что происходит на экране
во время записи. Кто именно превращает голос в текст — решается потом.
Готовые библиотеки уже найдены, см. `MIC-ASR-SEARCH.md`.
4. **Что делать с куском прерванного ответа** в кэше (`CACHE.md`, п. 3.2).
5. Где показывать несколько агентов в одном списке диалогов (сейчас список —
у активного агента).
+33
View File
@@ -131,6 +131,39 @@ Text(
Разбор — `BORROW-FROM-ASSISTENT.md`, п. 3.10.
## Токены тёмной темы (на ней строится палитра)
Это **эталонный набор** — какие цвета и с какими именами есть в тёмной теме.
В макетах (`sketches/`, `approved/`) они стоят числами в `:root { --token: #... }`,
в коде приложения — через `MaterialTheme.colorScheme.*`. Значения **согласованы
с Android-клиентом** (`agentik-android/design/BORROW.md:39`), плюс расширения,
которых в десктопной основе не было.
| Токен | Назначение | Значение | Откуда |
|---|---|---|---|
| `--bg` | фон окна | `#121218` | основа |
| `--panel` | панели (шапки, боковая) | `#17212B` | основа |
| `--panel2` | «приподнятые» элементы на панели | `#1C2733` | Android |
| `--line` | разделители и рамки полей | `#232B36` | Android |
| `--fg` | основной текст | `#EBEBEB` | основа |
| `--dim` | приглушённый текст (время, подписи) | `#768C9E` | основа |
| `--accent` | главный акцент | `#6AB2F2` | основа |
| `--badge` | фон кружка непрочитанных | `#5EB5F7` | Android |
| `--err` | ошибки, опасные зоны, запись | `#EC3942` | основа |
| `--ok` | «на связи», успех проверки | `#63C88A` | Android |
| `--user-bubble` | пузырь сообщения пользователя | `#2B5278` | Android |
| `--bot-bubble` | пузырь сообщения ассистента | `#1C2733` | Android |
| `--a1`..`--a5` | **пользовательские** цвета агентов | `#6AB2F2` `#86C99A` `#C9A886` `#B79AE0` `#E28C8C` | Android |
**Важно про `--a1`..`--a5`:** это **не токены темы**. Они заданы для набора
по умолчанию и подобраны под тёмную тему. В коде это **пользовательские**
значения из настроек агента — кладутся в конфиг и читаются как `agent.color`,
а не через `MaterialTheme.colorScheme`. Иначе при смене темы агенты перестанут
отличаться друг от друга. Подробности — раздел «Особый случай» выше.
**Цвет текста на аксентном фоне:** на `--accent` (`#6AB2F2`) и `--badge`
(`#5EB5F7`) текст берётся **тёмным** (`#0F1922`), иначе не читается.
## Что осталось решить
- **Какие темы будут кроме тёмной.** Пока известно только: тёмная есть, светлая
-413
View File
@@ -1,413 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Вариант 1 — Sidebar / Utility</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line);
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
/* ---------- левая колонка: список чатов ---------- */
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.icon-btn {
width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px;
}
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
.search {
margin: 10px 16px 8px; padding: 8px 12px; border-radius: 9px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13px; width: calc(100% - 32px); outline: none;
}
.search:focus { border-color: var(--accent); }
.search::placeholder { color: var(--dim); }
/* лента папок — как в мобильном клиенте */
.folders { display: flex; gap: 6px; padding: 6px 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
/* кнопка «Группы» — не папка, а действие: держим справа, отдельно от чипов */
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
.folder.act:hover { color: var(--fg); background: var(--panel2); }
.chat-list { flex: 1; overflow-y: auto; padding: 4px 8px 12px; }
.chat-row {
display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start;
}
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; } .avatar.g3 { background: #C9A886; } .avatar.g4 { background: #B79AE0; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; flex: 0 0 auto; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.badge {
background: var(--badge); color: #0F1922; border-radius: 10px; font-size: 11px; font-weight: 700;
padding: 1px 6px; min-width: 19px; text-align: center;
}
.row-tail { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: 0 0 auto; }
/* ---------- правая часть: чат ---------- */
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head {
position: relative;
height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center;
gap: 8px; padding: 0 16px;
}
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-meta { flex: 1; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: #63C88A; }
/* кнопка «назад» — нужна только в узком окне */
.back-btn {
display: none; width: 32px; height: 32px; flex: 0 0 32px; border-radius: 8px;
border: none; background: transparent; color: var(--fg); cursor: pointer;
font-size: 17px; place-items: center;
}
.back-btn:hover { background: var(--panel); }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
.msg .meta { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
/* блок «рассуждения» и «инструмент» — это специфика агента, не мессенджера */
.think {
align-self: flex-start; max-width: 80%; border-left: 2px solid var(--accent);
background: rgba(106,178,242,.07); padding: 8px 12px; border-radius: 0 10px 10px 0;
font-size: 12.5px; color: var(--dim); cursor: pointer;
}
.think b { color: var(--accent); font-weight: 600; }
.tool {
align-self: flex-start; max-width: 80%; background: var(--panel); border: 1px solid var(--line);
border-radius: 10px; padding: 8px 12px; font-size: 12.5px;
display: flex; align-items: center; gap: 9px;
}
.tool .gear { color: var(--accent); }
.tool code { color: var(--dim); font-size: 11.5px; }
/* ---------- панель ввода с кнопкой записи ---------- */
.composer { border-top: 1px solid var(--line); padding: 12px 16px 14px; background: var(--panel); }
.attach-strip { display: flex; gap: 8px; margin-bottom: 9px; }
.attach-strip:empty { display: none; }
.chip {
background: var(--panel2); border: 1px solid var(--line); border-radius: 8px;
padding: 5px 9px; font-size: 11.5px; color: var(--dim); display: flex; align-items: center; gap: 7px;
}
.chip .x { cursor: pointer; color: var(--err); font-weight: 700; }
.input-row { display: flex; align-items: flex-end; gap: 9px; }
.input-wrap { flex: 1; display: flex; align-items: flex-end; background: var(--bg); border: 1px solid var(--line);
border-radius: 11px; padding: 9px 12px; gap: 9px; }
.input-wrap:focus-within { border-color: var(--accent); }
textarea {
flex: 1; background: transparent; border: none; outline: none; resize: none; color: var(--fg);
font-family: inherit; font-size: 13.5px; line-height: 1.45; max-height: 120px; height: 22px;
}
textarea::placeholder { color: var(--dim); }
/* круглая кнопка записи */
.rec-btn {
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line); background: var(--bg);
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; flex: 0 0 40px;
transition: all .18s;
}
.rec-btn:hover { color: var(--fg); border-color: var(--dim); }
.rec-btn.recording { background: var(--err); border-color: var(--err); color: #fff; animation: pulse 1.4s infinite; }
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(236,57,66,.55); } 50% { box-shadow: 0 0 0 9px rgba(236,57,66,0); } }
.send-btn {
height: 40px; padding: 0 18px; border-radius: 11px; border: none; background: var(--accent);
color: #0F1922; font-size: 13px; font-weight: 600; cursor: pointer;
}
.send-btn:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
.hint { font-size: 11px; color: var(--dim); margin-top: 8px; display: flex; justify-content: space-between; }
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10.5px; }
/* запись: полоса над полем */
.rec-panel { display: none; align-items: center; gap: 12px; margin-bottom: 10px;
background: var(--bg); border: 1px solid var(--err); border-radius: 11px; padding: 9px 13px; }
.rec-panel.on { display: flex; }
.wave { display: flex; align-items: center; gap: 3px; height: 22px; flex: 1; }
.wave i { width: 3px; background: var(--err); border-radius: 2px; height: 30%; animation: wv .9s infinite ease-in-out; }
.wave i:nth-child(2n) { animation-delay: .12s; } .wave i:nth-child(3n) { animation-delay: .24s; }
.wave i:nth-child(4n) { animation-delay: .36s; }
@keyframes wv { 0%,100% { height: 22%; } 50% { height: 100%; } }
.rec-time { font-variant-numeric: tabular-nums; font-size: 13px; color: var(--err); font-weight: 600; }
.rec-actions { display: flex; gap: 8px; }
.mini { border: 1px solid var(--line); background: var(--panel2); color: var(--fg); border-radius: 8px;
padding: 5px 11px; font-size: 12px; cursor: pointer; }
.mini.ok { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
.mini.cancel { color: var(--err); }
/* всплывающее состояние: расшифровка распознанного текста */
.asr-note {
display: none; align-items: center; gap: 8px; font-size: 11.5px; color: var(--dim);
margin-bottom: 8px; padding-left: 2px;
}
.asr-note.on { display: flex; }
.asr-note .live { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
/* ---------- узкое окно: либо список, либо чат ---------- */
.backdrop {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 90;
}
.backdrop.on { display: block; }
@media (max-width: 880px) {
body { padding: 0; }
.window { width: 100vw; height: 100vh; border-radius: 0; border: none; transform: none !important; }
.window.mode-chat .sidebar { display: none; }
.window.mode-list .main { display: none; }
.sidebar { width: 100%; flex: 1 1 auto; border-right: none; }
.back-btn { display: grid; }
}
/* окно целиком влезает на любой экран — не режем картинку */
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
</style>
</head>
<body>
<div class="window">
<!-- ================= СПИСОК ЧАТОВ ================= -->
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="icon-btn" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…">
<div class="folders">
<div class="folder active">Все чаты <span class="cnt">3</span></div>
<div class="folder">Работа</div>
<div class="folder">Дом</div>
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
</div>
<div class="chat-list" id="chatList">
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g3">РЗ</div>
<div class="chat-mid">
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Нашёл 3 ошибки 502</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g4">ПО</div>
<div class="chat-mid">
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Девайс-нет стартовал</div>
</div>
</div>
</div>
</aside>
<!-- ================= ЧАТ ================= -->
<main class="main">
<header class="chat-head">
<button class="back-btn" id="backBtn" title="Назад к диалогам">←</button>
<div class="avatar">АГ</div>
<div class="head-meta">
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
<button class="icon-btn" title="Параметры диалога">⋯</button>
</header>
<div class="messages" id="messages">
<div class="msg user">Проверь, почему падает CI в agentik<span class="meta">14:18</span></div>
<div class="think" onclick="this.querySelector('.rest').style.display = this.querySelector('.rest').style.display==='none'?'inline':'none'">
<b>Думает:</b> 42 прогона, красных 40. Смотрю имена тестов… <span class="rest">похоже на проблему кодировки при создании файлов классов.</span>
</div>
<div class="tool"><span class="gear">⚙</span> <code>grep -rnP 'fun `[^`]*[^\x00-\x7F]' --include='*.kt'</code></div>
<div class="msg bot">Нашёл причину: в именах тестов стоит длинное тире «—», а сборка работает в ASCII-кодировке. Отсюда падение компилятора. Починил, CI зелёный.<span class="meta">14:22</span></div>
<div class="msg user">Отлично. Сделай релиз «3»<span class="meta">14:23</span></div>
<div class="think" onclick="this.querySelector('.rest').style.display = this.querySelector('.rest').style.display==='none'?'inline':'none'">
<b>Думает:</b> создаю тег, запускаю публикацию… <span class="rest">проверю, что в Nexus появилась версия 3.</span>
</div>
</div>
<!-- ===== панель ввода ===== -->
<div class="composer">
<div class="attach-strip" id="chips"></div>
<div class="asr-note" id="asrNote">
<span class="live"></span> распознаю: <span id="asrText" style="color:var(--fg)"></span>
</div>
<div class="rec-panel" id="recPanel">
<button class="mini cancel" onclick="stopRec(true)">✕</button>
<div class="wave" id="wave"></div>
<span class="rec-time" id="recTime">0:00</span>
<div class="rec-actions">
<button class="mini ok" onclick="stopRec(false)">Остановить и отправить</button>
</div>
</div>
<div class="input-row" id="inputRow">
<div class="input-wrap">
<textarea id="ta" rows="1" placeholder="Написать агенту…"></textarea>
</div>
<button class="rec-btn" id="recBtn" title="Записать голосовое (Ctrl+R)">🎙</button>
<button class="send-btn" id="sendBtn" disabled>Отправить</button>
</div>
<div class="hint">
<span id="statusHint"></span>
</div>
</div>
</main>
</div>
<script>
const ta = document.getElementById('ta');
const sendBtn = document.getElementById('sendBtn');
const recBtn = document.getElementById('recBtn');
const recPanel = document.getElementById('recPanel');
const inputRow = document.getElementById('inputRow');
const messages = document.getElementById('messages');
const asrNote = document.getElementById('asrNote');
const asrText = document.getElementById('asrText');
const statusHint = document.getElementById('statusHint');
// волна из 28 столбиков
document.getElementById('wave').innerHTML = Array.from({length: 28}, () => '<i></i>').join('');
ta.addEventListener('input', () => {
ta.style.height = '22px';
ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
sendBtn.disabled = ta.value.trim() === '';
});
function send() {
const t = ta.value.trim(); if (!t) return;
addMsg(t, 'user');
ta.value = ''; ta.style.height = '22px'; sendBtn.disabled = true;
statusHint.textContent = 'агент думает…';
setTimeout(() => {
addMsg('Принято. Выполняю: «' + t + '» — покажу ход в этом же диалоге.', 'bot');
statusHint.textContent = 'готов';
}, 900);
}
function addMsg(text, who) {
const d = document.createElement('div');
d.className = 'msg ' + who;
const now = new Date();
d.innerHTML = text + '<span class="meta">' + now.getHours() + ':' + String(now.getMinutes()).padStart(2,'0') + '</span>';
messages.appendChild(d); messages.scrollTop = messages.scrollHeight;
}
sendBtn.onclick = send;
ta.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
// ---------- запись ----------
let recTimer = null, recSec = 0;
const WORDS = ['проверь','логи','сборки','за','вчерашний','день','и','сделай','вывод'];
function startRec() {
recSec = 0; recPanel.classList.add('on'); recBtn.classList.add('recording');
inputRow.style.opacity = '.45'; statusHint.textContent = 'запись…';
asrNote.classList.add('on'); asrText.textContent = '';
let i = 0;
recTimer = setInterval(() => {
recSec++;
document.getElementById('recTime').textContent = '0:' + String(recSec).padStart(2,'0');
if (i < WORDS.length && recSec % 1 === 0) { // живая расшифровка
asrText.textContent = WORDS.slice(0, ++i).join(' ') + (i < WORDS.length ? ' …' : '');
}
}, 1000);
}
function stopRec(cancel) {
clearInterval(recTimer);
recPanel.classList.remove('on'); recBtn.classList.remove('recording');
inputRow.style.opacity = '1'; asrNote.classList.remove('on');
if (cancel) { statusHint.textContent = 'запись отменена'; return; }
if (recSec < 1) { statusHint.textContent = 'слишком коротко'; return; }
ta.value = WORDS.join(' ') + '.'; // распознанный текст попадает в поле
ta.dispatchEvent(new Event('input'));
statusHint.textContent = 'распознано, ' + recSec + ' с — проверь и отправь';
ta.focus();
}
recBtn.onclick = () => recPanel.classList.contains('on') ? stopRec(false) : startRec();
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); recBtn.click(); }
});
// ---------- широкое / узкое окно ----------
const win = document.querySelector('.window');
const NARROW = 880;
function applyMode() {
if (innerWidth > NARROW) { // широкое: видно и то, и другое
win.classList.remove('mode-chat', 'mode-list');
return;
}
// узкое: показываем что-то одно, по умолчанию список
if (!win.classList.contains('mode-chat') && !win.classList.contains('mode-list')) {
win.classList.add('mode-list');
}
}
document.querySelectorAll('.chat-row').forEach(r => r.addEventListener('click', () => {
document.querySelectorAll('.chat-row').forEach(x => x.classList.remove('active'));
r.classList.add('active');
if (innerWidth <= NARROW) win.classList.remove('mode-list'), win.classList.add('mode-chat');
}));
document.getElementById('backBtn').onclick = () => win.classList.remove('mode-chat'), win.classList.add('mode-list');
addEventListener('resize', applyMode);
applyMode();
</script>
</body>
</html>
-404
View File
@@ -1,404 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Одобрено: Агенты — список, форма, проверка связи</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2;
--err: #EC3942; --ok: #63C88A;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
position: relative;
}
/* ============ затемнение: чуть гасит окно под окном проверки ============ */
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 200; display: none; }
.scrim.on { display: block; }
/* ============ ОКНО 1: НАСТРОЙКИ ============ */
.dialog {
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7); position: relative; z-index: 150;
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* ── список агентов ── */
.agent-card {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.agent-card:hover { border-color: var(--line); }
.avatar {
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 13px; font-weight: 600; color: #0F1922; background: var(--accent); overflow: hidden;
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
/* значок-картинка: клиент показывает свою копию файла */
.avatar.pic { background: transparent; padding: 0; }
.avatar.pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ag-mid { flex: 1; min-width: 0; }
.ag-name { font-size: 14px; font-weight: 550; }
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* ── форма ── */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
.note code { background: var(--panel2); padding: 1px 4px; border-radius: 4px; font-size: 10.5px; }
.addr-row { display: flex; gap: 8px; }
.addr-row input { flex: 1; }
/* ── цвет ИЛИ картинка ── */
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
.appearance-tabs .tab {
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
}
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.pane { display: none; } .pane.on { display: block; }
.colors { display: flex; gap: 9px; }
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
.swatch.on { border-color: var(--fg); }
.pic-row { display: flex; align-items: center; gap: 13px; }
.pic-preview {
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
font-size: 20px; overflow: hidden;
}
.pic-preview.filled { border-style: solid; }
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
/* ── кнопки ── */
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.prim:disabled { background: var(--panel2); border-color: var(--line); color: var(--dim); cursor: default; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ============ ОКНО 2: ПРОВЕРКА СВЯЗИ (поверх настроек) ============ */
.layer { position: fixed; inset: 0; display: none; align-items: center; justify-content: center;
z-index: 250; pointer-events: none; }
.layer.on { display: flex; }
.layer > * { pointer-events: auto; }
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
@keyframes sp { to { transform: rotate(360deg); } }
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 19px; flex: 0 0 40px; }
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
.probe-head.busy .big { background: transparent; padding: 0; }
.probe-head .txt { flex: 1; }
.probe-head .ttl { font-size: 14px; font-weight: 600; }
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px; }
.probe-list { font-size: 12px; }
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
border-bottom: 1px solid var(--line); }
.probe-list div:last-child { border-bottom: none; }
.probe-list span { color: var(--dim); }
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; }
.hint-line { font-size: 11.5px; color: var(--dim); margin-top: 12px; padding: 9px 11px;
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); }
</style>
</head>
<body>
<!-- ================= ОКНО 1: НАСТРОЙКИ ================= -->
<div class="dialog">
<div class="d-head">
<h2 id="dTitle">Агенты</h2>
<button class="x-btn">✕</button>
</div>
<!-- ── вид 1: список ── -->
<div class="d-body" id="viewList">
<div class="agent-card">
<div class="avatar">АГ</div>
<div class="ag-mid">
<div class="ag-name">agentik · рабочий</div>
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
</div>
<div class="state"><span class="d"></span>на связи</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.88.135:8080/agentik',true)">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="agent-card">
<div class="avatar g2">ДМ</div>
<div class="ag-mid">
<div class="ag-name">домашний</div>
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
</div>
<div class="state err"><span class="d"></span>не отвечает</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.76.199:8080/agentik',false)">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="agent-card">
<div class="avatar pic"><img src="data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E" alt=""></div>
<div class="ag-mid">
<div class="ag-name">Кот-ассистент</div>
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
</div>
<div class="state off"><span class="d"></span>не проверялся</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.88.24:8080/agentik',true)">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<button class="btn wide" onclick="show('add')">+ Добавить агента</button>
</div>
<!-- ── вид 2: форма нового ── -->
<div class="d-body" id="viewAdd" style="display:none">
<div class="field">
<label>Название</label>
<input type="text" value="рабочий" placeholder="как вы его будете звать">
<div class="note">Как называть этого агента в списке. Придумываете сами — сервер своего имени не сообщает.</div>
</div>
<div class="field">
<label>Адрес сервера агента</label>
<div class="addr-row">
<input type="text" class="mono" id="addrField" value="http://192.168.88.135:8080/agentik"
placeholder="http://хост:порт/agentik">
<button class="btn sm" onclick="openCheck('form', document.getElementById('addrField').value, true)">Проверить связь</button>
</div>
<div class="note">Адрес целиком, вместе с путём <code>/agentik</code>. Если агент
на этой же машине — <code>http://localhost:8080/agentik</code>.</div>
</div>
<div class="field">
<label>Как отличать в списке</label>
<div class="appearance-tabs">
<div class="tab on" data-tab="color">Цвет</div>
<div class="tab" data-tab="pic">Картинка</div>
</div>
<div class="pane on" id="paneColor">
<div class="colors">
<div class="swatch on" style="background:#6AB2F2"></div>
<div class="swatch" style="background:#86C99A"></div>
<div class="swatch" style="background:#C9A886"></div>
<div class="swatch" style="background:#B79AE0"></div>
<div class="swatch" style="background:#E28C8C"></div>
</div>
<div class="note">Цветом помечаются диалоги агента в списке — полоска у строки
и точка у группы.</div>
</div>
<div class="pane" id="panePic">
<div class="pic-row">
<div class="pic-preview" id="picPreview">🖼</div>
<div class="pic-actions">
<button class="btn sm" onclick="pickPic()">Выбрать файл…</button>
<button class="btn sm" id="picClear" onclick="clearPic()" style="display:none">Убрать</button>
</div>
</div>
<div class="note">Картинка копируется <b>внутрь клиента</b> — по исходному адресу
не подтягивается. Иначе значок пропадёт, если файл удалят, флешку вынут
или папку переименуют.</div>
</div>
</div>
</div>
<div class="d-foot" id="footList">
<button class="btn prim" style="flex:1">Готово</button>
</div>
<div class="d-foot" id="footAdd" style="display:none">
<button class="btn" style="flex:1" onclick="show('list')">Отмена</button>
<button class="btn prim" style="flex:1">Добавить агента</button>
</div>
</div>
<div class="scrim" id="scrim"></div>
<!-- ================= ОКНО 2: ПРОВЕРКА СВЯЗИ ================= -->
<div class="layer" id="layerCheck">
<div class="dialog small">
<div class="d-head">
<h2>Проверка связи</h2>
<button class="x-btn" onclick="closeCheck()">✕</button>
</div>
<div class="d-body">
<div class="probe-head" id="probeHead">
<div class="big" id="probeIcon"></div>
<div class="txt">
<div class="ttl" id="probeTitle">Спрашиваю агента…</div>
<div class="sub" id="probeSub">http://192.168.88.135:8080/agentik</div>
</div>
</div>
<div class="probe-list" id="probeList" style="display:none">
<div><span>Ответ на запрос</span><b id="p1">—</b></div>
<div><span>Отвечает по адресу</span><b id="p2">—</b></div>
<div><span>Диалогов у агента</span><b id="p3">—</b></div>
<div><span>Отклик</span><b id="p4">—</b></div>
</div>
<div class="hint-line" id="probeHint" style="display:none"></div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1" id="probeAgain" onclick="runCheck()">Проверить снова</button>
<button class="btn prim" style="flex:1" id="probeOk" disabled>Добавить агента</button>
</div>
</div>
</div>
<script>
function el(id){ return document.getElementById(id); }
// ── переключение список / форма ──
function show(v) {
const isList = (v === 'list');
el('viewList').style.display = isList ? 'block' : 'none';
el('viewAdd').style.display = isList ? 'none' : 'block';
el('footList').style.display = isList ? 'flex' : 'none';
el('footAdd').style.display = isList ? 'none' : 'flex';
el('dTitle').textContent = isList ? 'Агенты' : 'Новый агент';
}
// ── цвет ИЛИ картинка ──
document.querySelectorAll('.appearance-tabs .tab').forEach(tab => tab.onclick = () => {
document.querySelectorAll('.appearance-tabs .tab').forEach(x => x.classList.remove('on'));
tab.classList.add('on');
const pic = (tab.dataset.tab === 'pic');
el('paneColor').classList.toggle('on', !pic);
el('panePic').classList.toggle('on', pic);
});
document.querySelectorAll('.swatch').forEach(s => s.onclick = () => {
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('on'));
s.classList.add('on');
});
// выбор картинки: в настоящем клиенте — системный диалог выбора файла,
// а выбранный файл КОПИРУЕТСЯ в папку клиента (не ссылка на исходный путь)
function pickPic() {
setPic('data:image/svg+xml;utf8,' + encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54">
<rect width="54" height="54" fill="#86C99A"/>
<circle cx="27" cy="21" r="8" fill="#121218"/>
<path d="M10 50 Q27 30 44 50 Z" fill="#121218"/>
</svg>`));
}
function setPic(src) {
const p = el('picPreview');
p.innerHTML = `<img src="${src}" alt="">`;
p.classList.add('filled');
el('picClear').style.display = '';
}
function clearPic() {
const p = el('picPreview');
p.innerHTML = '🖼';
p.classList.remove('filled');
el('picClear').style.display = 'none';
}
// ── ОКНО ПРОВЕРКИ: открывается из формы или из списка ──
let checkAddr = '', checkOk = true;
function openCheck(from, addr, ok) {
checkAddr = addr; checkOk = ok;
el('probeOk').textContent = (from === 'form') ? 'Добавить агента' : 'Готово';
el('scrim').classList.add('on');
el('layerCheck').classList.add('on');
runCheck();
}
function closeCheck() {
el('layerCheck').classList.remove('on');
el('scrim').classList.remove('on');
}
function runCheck() {
const list = el('probeList'), hint = el('probeHint');
el('probeHead').className = 'probe-head busy';
el('probeIcon').outerHTML = '<span class="spin" id="probeIcon"></span>';
el('probeTitle').textContent = 'Спрашиваю агента…';
el('probeSub').textContent = checkAddr;
list.style.display = 'none';
hint.style.display = 'none';
el('probeOk').disabled = true;
setTimeout(() => {
document.getElementById('probeIcon').outerHTML = '<div class="big" id="probeIcon"></div>';
el('probeSub').textContent = checkAddr;
if (!checkOk) {
el('probeHead').className = 'probe-head err';
el('probeIcon').textContent = '✕';
el('probeTitle').textContent = 'Агент не отвечает';
el('p1').textContent = 'нет ответа'; el('p2').textContent = '—';
el('p3').textContent = '—'; el('p4').textContent = '5000 мс (истекло)';
list.style.display = 'block'; hint.style.display = 'block';
hint.textContent = 'Проверьте адрес и что агент запущен. Убедитесь, что порт открыт и путь /agentik указан целиком.';
el('probeOk').disabled = true;
} else {
el('probeHead').className = 'probe-head ok';
el('probeIcon').textContent = '✓';
el('probeTitle').textContent = 'Агент отвечает';
el('p1').textContent = 'ok'; el('p2').textContent = checkAddr;
el('p3').textContent = '4'; el('p4').textContent = '38 мс';
list.style.display = 'block'; hint.style.display = 'block';
hint.textContent = 'Адрес рабочий. Можно добавлять этого агента.';
el('probeOk').disabled = false;
}
}, 1400);
}
</script>
</body>
</html>
-144
View File
@@ -1,144 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Одобрено: Новый диалог — выбор ассистента</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --err: #EC3942; --ok: #63C88A;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
.dialog {
width: 480px; min-width: 480px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
.agent-opt {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; cursor: pointer; border: 1.5px solid transparent;
}
.agent-opt:hover { border-color: #33414F; }
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
.avatar {
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 13px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
/* аватар-картинка: клиент показывает свою копию файла */
.avatar.pic {
background: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E") center/cover;
}
.ag-mid { flex: 1; min-width: 0; }
.ag-name { font-size: 14px; font-weight: 550; }
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
.ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
.ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.ag-state.ok { color: var(--ok); }
.ag-state.off { color: var(--dim); } .ag-state.off .d { background: var(--dim); }
.ag-state.err { color: var(--err); } .ag-state.err .d { background: var(--err); }
.pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%; border: 2px solid #3D4A58; }
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
.agent-opt.on .pick::after {
content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 9px;
border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg);
}
.warn {
display: none; gap: 8px; align-items: flex-start; margin-top: 4px; padding: 10px 12px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA;
}
.warn.on { display: flex; }
.btn {
padding: 10px 16px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel2);
color: var(--fg); font-size: 13.5px; cursor: pointer; font-family: inherit;
}
.btn:hover { border-color: #3D4A58; }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
</style>
</head>
<body>
<div class="dialog">
<div class="d-head">
<h2>Новый диалог</h2>
<button class="x-btn">✕</button>
</div>
<div class="d-body">
<div class="d-lead">В каком ассистенте создать диалог?</div>
<div class="agent-opt on" data-ok="1">
<div class="avatar">АГ</div>
<div class="ag-mid">
<div class="ag-name">agentik · рабочий</div>
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
<div class="ag-state ok"><span class="d"></span>на связи</div>
</div>
<div class="pick"></div>
</div>
<div class="agent-opt" data-ok="1">
<div class="avatar g2">ДМ</div>
<div class="ag-mid">
<div class="ag-name">домашний</div>
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
<div class="ag-state ok"><span class="d"></span>на связи</div>
</div>
<div class="pick"></div>
</div>
<div class="agent-opt" data-ok="0">
<div class="avatar pic"></div>
<div class="ag-mid">
<div class="ag-name">Кот-ассистент</div>
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
<div class="ag-state err"><span class="d"></span>не отвечает</div>
</div>
<div class="pick"></div>
</div>
<div class="warn" id="warn">
<span>⚠</span>
<span>Ассистент сейчас не отвечает. Диалог создать можно, но
сообщение не уйдёт, пока он не поднимется.</span>
</div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1">Отмена</button>
<button class="btn prim" style="flex:1">Создать диалог</button>
</div>
</div>
<script>
// Выбор ассистента. Первый отмечен сразу — создать диалог можно одним нажатием.
document.querySelectorAll('.agent-opt').forEach(o => o.onclick = () => {
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
o.classList.add('on');
document.getElementById('warn').classList.toggle('on', o.dataset.ok === '0');
});
</script>
</body>
</html>
-296
View File
@@ -1,296 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Группы — список</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
/* ============ окно клиента на фоне ============ */
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
.search {
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.search::placeholder { color: var(--dim); }
/* ── лента групп ── */
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
.folder.act:hover { color: var(--fg); background: var(--panel2); }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
.avatar.g4 { background: #B79AE0; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin-top: 2px; }
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
border-radius: 10px; padding: 1px 7px; }
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
/* ============ затемнение и модалка ============ */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
.dialog {
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* ── строка группы ── */
.grp {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.grp:hover { border-color: var(--line); }
.grp .mid { flex: 1; min-width: 0; }
.grp .nm { font-size: 14px; font-weight: 550; }
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* служебная группа — «Все чаты»: не правится и не удаляется */
.grp.sys { background: transparent; border: 1px dashed var(--line); }
.grp.sys .nm { color: var(--fg); }
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ── форма ── */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
/* ── предупреждение ── */
.warn {
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA; line-height: 1.5;
}
.warn .i { flex: 0 0 auto; font-size: 13px; }
.info {
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
font-size: 11.5px; color: var(--dim); line-height: 1.5;
}
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
</style>
</head>
<body>
<div class="window">
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="icon-btn" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…">
<div class="folders">
<div class="folder active">Все чаты <span class="cnt">3</span></div>
<div class="folder">Работа</div>
<div class="folder">Дом</div>
<div class="folder">Учёба</div>
<div class="folder">Личное</div>
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
</div>
<div class="chat-list">
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g3">РЗ</div>
<div class="chat-mid">
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Нашёл 3 ошибки 502</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g4">ПО</div>
<div class="chat-mid">
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Девайт-нет стартовал</div>
</div>
</div></div>
</aside>
<main class="main">
<header class="chat-head">
<div class="avatar">АГ</div>
<div>
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
</header>
<div class="messages">
<div class="msg user">Проверь, почему падает CI</div>
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
</div>
</main>
<div class="scrim"></div>
<div class="layer">
<div class="dialog">
<div class="d-head">
<h2>Группы</h2>
<button class="x-btn">✕</button>
</div>
<div class="d-body">
<div class="grp sys">
<div class="mid">
<div class="nm">Все чаты</div>
<div class="sub">Показывает все диалоги, из любой группы</div>
</div>
<div class="lock">встроенная</div>
</div>
<div class="grp">
<div class="mid">
<div class="nm">Работа</div>
<div class="sub">12 диалогов</div>
</div>
<div class="acts">
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="grp">
<div class="mid">
<div class="nm">Дом</div>
<div class="sub">4 диалога</div>
</div>
<div class="acts">
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="grp">
<div class="mid">
<div class="nm">Учёба</div>
<div class="sub">2 диалога</div>
</div>
<div class="acts">
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="grp">
<div class="mid">
<div class="nm">Личное</div>
<div class="sub">пустая</div>
</div>
<div class="acts">
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<button class="btn wide">+ Добавить группу</button>
</div>
<div class="d-foot">
<button class="btn prim" style="flex:1">Готово</button>
</div>
</div>
</div>
</div>
</body>
</html>
-255
View File
@@ -1,255 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Группы — новая</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
/* ============ окно клиента на фоне ============ */
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
.search {
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.search::placeholder { color: var(--dim); }
/* ── лента групп ── */
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
.folder.act:hover { color: var(--fg); background: var(--panel2); }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
.avatar.g4 { background: #B79AE0; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin-top: 2px; }
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
border-radius: 10px; padding: 1px 7px; }
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
/* ============ затемнение и модалка ============ */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
.dialog {
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* ── строка группы ── */
.grp {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.grp:hover { border-color: var(--line); }
.grp .mid { flex: 1; min-width: 0; }
.grp .nm { font-size: 14px; font-weight: 550; }
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* служебная группа — «Все чаты»: не правится и не удаляется */
.grp.sys { background: transparent; border: 1px dashed var(--line); }
.grp.sys .nm { color: var(--fg); }
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ── форма ── */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
/* ── предупреждение ── */
.warn {
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA; line-height: 1.5;
}
.warn .i { flex: 0 0 auto; font-size: 13px; }
.info {
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
font-size: 11.5px; color: var(--dim); line-height: 1.5;
}
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
</style>
</head>
<body>
<div class="window">
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="icon-btn" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…">
<div class="folders">
<div class="folder active">Все чаты <span class="cnt">3</span></div>
<div class="folder">Работа</div>
<div class="folder">Дом</div>
<div class="folder">Учёба</div>
<div class="folder">Личное</div>
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
</div>
<div class="chat-list">
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g3">РЗ</div>
<div class="chat-mid">
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Нашёл 3 ошибки 502</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g4">ПО</div>
<div class="chat-mid">
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Девайт-нет стартовал</div>
</div>
</div></div>
</aside>
<main class="main">
<header class="chat-head">
<div class="avatar">АГ</div>
<div>
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
</header>
<div class="messages">
<div class="msg user">Проверь, почему падает CI</div>
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
</div>
</main>
<div class="scrim"></div>
<div class="layer">
<div class="dialog">
<div class="d-head">
<h2>Новая группа</h2>
<button class="x-btn">✕</button>
</div>
<div class="d-body">
<div class="field">
<label>Название</label>
<input type="text" value="Учёба" placeholder="как назвать группу">
<div class="note">Так группа будет называться в ленте над списком диалогов.</div>
</div>
<div class="info">
<span>ℹ</span>
<span>Диалоги в группу попадают потом — из самого диалога. Здесь группа
только создаётся, диалог в неё не переносится.</span>
</div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1">Отмена</button>
<button class="btn prim" style="flex:1">Создать группу</button>
</div>
</div>
</div>
</div>
</body>
</html>
-260
View File
@@ -1,260 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Группы — изменение</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
/* ============ окно клиента на фоне ============ */
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
.search {
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.search::placeholder { color: var(--dim); }
/* ── лента групп ── */
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
.folder.act:hover { color: var(--fg); background: var(--panel2); }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
.avatar.g4 { background: #B79AE0; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin-top: 2px; }
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
border-radius: 10px; padding: 1px 7px; }
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
/* ============ затемнение и модалка ============ */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
.dialog {
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* ── строка группы ── */
.grp {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.grp:hover { border-color: var(--line); }
.grp .mid { flex: 1; min-width: 0; }
.grp .nm { font-size: 14px; font-weight: 550; }
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* служебная группа — «Все чаты»: не правится и не удаляется */
.grp.sys { background: transparent; border: 1px dashed var(--line); }
.grp.sys .nm { color: var(--fg); }
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ── форма ── */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
/* ── предупреждение ── */
.warn {
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA; line-height: 1.5;
}
.warn .i { flex: 0 0 auto; font-size: 13px; }
.info {
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
font-size: 11.5px; color: var(--dim); line-height: 1.5;
}
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
</style>
</head>
<body>
<div class="window">
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="icon-btn" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…">
<div class="folders">
<div class="folder">Все чаты <span class="cnt">3</span></div>
<div class="folder active">Работа</div>
<div class="folder">Дом</div>
<div class="folder">Учёба</div>
<div class="folder">Личное</div>
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
</div>
<div class="chat-list">
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g3">РЗ</div>
<div class="chat-mid">
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Нашёл 3 ошибки 502</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g4">ПО</div>
<div class="chat-mid">
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Девайт-нет стартовал</div>
</div>
</div></div>
</aside>
<main class="main">
<header class="chat-head">
<div class="avatar">АГ</div>
<div>
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
</header>
<div class="messages">
<div class="msg user">Проверь, почему падает CI</div>
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
</div>
</main>
<div class="scrim"></div>
<div class="layer">
<div class="dialog">
<div class="d-head">
<h2>Группа «Работа»</h2>
<button class="x-btn">✕</button>
</div>
<div class="d-body">
<div class="field">
<label>Название</label>
<input type="text" value="Работа" placeholder="как назвать группу">
<div class="note">Так группа будет называться в ленте над списком диалогов.</div>
</div>
<div class="info">
<span>ℹ</span>
<span>В группе сейчас 12 диалогов. Переименование их не затрагивает —
это только название группы.</span>
</div>
<div class="danger-zone">
<div class="lbl">Опасное</div>
<button class="btn dang" style="width:100%">Удалить группу</button>
</div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1">Отмена</button>
<button class="btn prim" style="flex:1">Сохранить</button>
</div>
</div>
</div>
</div>
</body>
</html>
-267
View File
@@ -1,267 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Группы — удаление</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
/* ============ окно клиента на фоне ============ */
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
.search {
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.search::placeholder { color: var(--dim); }
/* ── лента групп ── */
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
.folder.act:hover { color: var(--fg); background: var(--panel2); }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
.avatar.g4 { background: #B79AE0; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin-top: 2px; }
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
border-radius: 10px; padding: 1px 7px; }
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
/* ============ затемнение и модалка ============ */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
.dialog {
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* ── строка группы ── */
.grp {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.grp:hover { border-color: var(--line); }
.grp .mid { flex: 1; min-width: 0; }
.grp .nm { font-size: 14px; font-weight: 550; }
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* служебная группа — «Все чаты»: не правится и не удаляется */
.grp.sys { background: transparent; border: 1px dashed var(--line); }
.grp.sys .nm { color: var(--fg); }
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ── форма ── */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
/* ── предупреждение ── */
.warn {
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA; line-height: 1.5;
}
.warn .i { flex: 0 0 auto; font-size: 13px; }
.info {
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
font-size: 11.5px; color: var(--dim); line-height: 1.5;
}
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
</style>
</head>
<body>
<div class="window">
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="icon-btn" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…">
<div class="folders">
<div class="folder">Все чаты <span class="cnt">3</span></div>
<div class="folder active">Работа</div>
<div class="folder">Дом</div>
<div class="folder">Учёба</div>
<div class="folder">Личное</div>
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
</div>
<div class="chat-list">
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g3">РЗ</div>
<div class="chat-mid">
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Нашёл 3 ошибки 502</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g4">ПО</div>
<div class="chat-mid">
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Девайт-нет стартовал</div>
</div>
</div></div>
</aside>
<main class="main">
<header class="chat-head">
<div class="avatar">АГ</div>
<div>
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
</header>
<div class="messages">
<div class="msg user">Проверь, почему падает CI</div>
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
</div>
</main>
<div class="scrim"></div>
<div class="layer">
<div class="dialog">
<div class="d-head">
<h2>Удалить группу?</h2>
<button class="x-btn">✕</button>
</div>
<div class="d-body">
<div class="warn">
<span class="i">⚠</span>
<span>Группа <b>«Работа»</b> исчезнет из ленты. <b>Диалоги не удалятся</b> —
они останутся и будут видны в «Все чаты». Вернуть группу можно, создав её
заново, но диалоги придётся разложить руками.</span>
</div>
<div class="field">
<label>Что сделать с диалогами группы</label>
<div class="grp" style="margin-bottom:9px">
<div class="mid">
<div class="nm">Оставить без группы</div>
<div class="sub">12 диалогов вернутся в «Все чаты»</div>
</div>
<div class="lock">✓</div>
</div>
</div>
<div class="info">
<span>ℹ</span>
<span>Другого пути нет: диалог обязан быть где-то виден. Отдельной
«корзины» у диалогов нет — удаляются только диалоги, и не отсюда.</span>
</div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1">Отмена</button>
<button class="btn dang solid" style="flex:1">Удалить группу</button>
</div>
</div>
</div>
</div>
</body>
</html>
-100
View File
@@ -1,100 +0,0 @@
# 006 — Управление группами
**Иконка в ленте.** Над списком диалогов, справа от чипов групп, стоит **карандаш**
(✎) — вместо прежнего «+». Он означает **«Группы»**: открывает окно, где группы
можно добавить, переименовать и удалить. Раньше там был «+» — он читался как
«создать диалог», хотя диалог создаётся кнопкой в шапке списка.
Почему карандаш: рядом стоят чипы самих групп, и **всё, что похоже на «+», рядом
с ними читается как «добавить группу»**, а не «управлять ими». Карандаш говорит:
«здесь правят список групп целиком». Кнопка стоит отдельно от чипов — прижата
к правому краю, чтобы её не приняли за шестую группу.
---
## Файлы — по состояниям, без интерактивности
Интерактивность решено не делать: вместо неё **отдельный файл на каждое состояние**.
Открываются по одному, каждый показывает ровно одно окно.
| Файл | Что показывает |
|---|---|
| `1-list.html` | Список групп: каждая строка с «изменить» и «удалить», внизу «+ Добавить группу» |
| `2-add.html` | Новая группа: только название |
| `3-edit.html` | Изменение группы: переименование и удаление |
| `4-delete.html` | Удаление группы: что станет с диалогами |
---
## Окно «Группы» (1-list)
Строка группы: **название**, под ним — **сколько диалогов**, справа кнопки
**изменить (✎)** и **удалить (🗑)**. Внизу пунктирная **«+ Добавить группу»**.
**«Все чаты» — встроенная группа.** Она есть всегда, показана пунктирной рамкой
и помечена «встроенная»: её нельзя переименовать или удалить. Она нужна как
надежное место, где видно все диалоги сразу, — иначе при удалении группы диалоги
могли бы пропасть из виду. Кнопок правки у неё нет — не потому что забыли,
а потому что нечего править.
**Пустая группа не запрещена** — «Личное» в макете пустая, помечена словом
«пустая». Группу можно создать заранее и разложить диалоги потом.
---
## Новая группа (2-add)
**Одно поле — название.** Никаких настроек: цвет группы, порядок, значки —
не выдумываем, пока не попросили.
Под полем честная подсказка: **диалоги в группу попадают потом, из самого диалога.**
Здесь группа только создаётся — диалог в неё не переносится. Иначе пользователь
ждёт, что после создания в группе что-то появится, и не понимает, почему пусто.
---
## Изменение группы (3-edit)
**Переименование** — то же поле, что при создании, плюс подсказка, сколько
диалогов в группе сейчас (**переименование диалогов не касается**).
**Удаление — внизу, отдельным блоком «ОПАСНОЕ».** Так сделано нарочно: удаление
не должно стоять в одном ряду с «Сохранить», иначе промахнуться легко. Кнопка
красная, но не залитая — заливка появляется на следующем шаге, где спрашивают
подтверждение.
---
## Удаление группы (4-delete)
**Главное, что говорит окно: диалоги не удалятся.** Группа исчезнет из ленты,
диалоги останутся и будут видны в «Все чаты». Это первое, что написано
в предупреждении, — потому что именно этого боятся, нажимая «удалить».
Отдельный вопрос — **что сделать с диалогами**: оставить без группы (вернутся
в «Все чаты»). Другого пути нет: диалог обязан быть где-то виден, «корзины»
у диалогов не существует. Так и написано в окне — чтобы не выглядело, будто
выбор есть, а его нет.
Кнопка подтверждения — **красная залитая**, в тексте прямо «Удалить группу».
---
## Что решено и почему
- **Карандаш вместо «+».** Иначе две кнопки подряд означают «создать» — и в шапке
списка, и в ленте групп.
- **«Все чаты» не удаляется.** Нужна точка опоры, где собран весь список.
- **Диалоги не удаляются вместе с группой.** Иначе одно нажатие стирает работу.
- **Группа без диалогов допустима.** Создать заранее — нормальный сценарий.
- **Никаких настроек у группы, кроме названия.** Цвет, значок, порядок — не
просили, не делаем.
## Открытые вопросы
- **Куда диалог кладётся при создании** — в текущую открытую группу или без
группы? Сейчас в макете не показано.
- **Можно ли диалог быть в нескольких группах** (как ярлык в почте) или только
в одной? От этого зависит, что писать в удалении.
- **Порядок групп** — по алфавиту, по времени создания или перетаскиванием?
- **Что если удалить группу, в которой открыт диалог.** Сейчас не показано.
-145
View File
@@ -1,145 +0,0 @@
# Одобренные макеты
**Здесь только принятое.** Это не «вариант для сравнения» и не черновик — это
решение, на которое опираемся в реализации. Правило простое: одобренный макет
**живёт здесь**, а не остаётся «выбранным» среди вариантов в `sketches/`.
| Файл | Что это | Одобрено |
|---|---|---|
| `001-sidebar-utility.html` | **Основа дизайна**: привычный мессенджер | 2026-09-18 |
| `004-settings-agents.html` | Настройки агентов: список, форма, проверка связи | 2026-09-19 |
| `005-new-chat-picker.html` | Модалка «Новый диалог»: выбор ассистента | 2026-09-19 |
| `006-groups-manage/` | Управление группами — 4 файла по состояниям | 2026-09-19 |
`sketches/` — предложения: `002-rail-voice-first`, `003-three-pane-command`,
`004-settings-agents`, `005-new-chat-picker` (черновики, из них выросли одобренные).
---
## 001-sidebar-utility.html — основа дизайна
**Это база всего клиента.** Привычный мессенджер: список диалогов слева (320 px),
чат справа. Узкое окно — как в Телеграме: на экране что-то одно, у открытого
диалога сверху кнопка «Назад».
**Что в нём есть:**
- **лента папок** — «Все чаты / Работа / Дом / +», со счётчиком непрочитанных;
- поиск диалога;
- список диалогов: значок, название, последняя строка, время, кружок непрочитанных;
- шапка чата со значком, именем и состоянием связи;
- сообщения: пользователь справа, ассистент слева;
- поле ввода с кнопкой отправки и круглой кнопкой записи голоса.
**Из него убрано при одобрении:**
- подсказка под полем ввода;
- кольцо токенов — в библиотеке `client` таких данных нет, а «нет данных — нет цифры».
Стиль: фон `#121218`, панели `#17212B`, акцент `#6AB2F2`, текст `#EBEBEB`.
---
## 005-new-chat-picker.html — Новый диалог
**Модалка появляется по «+» (новый диалог), когда ассистентов больше одного.**
Состоит из одного окна 480 px и больше ничего: заголовок «Новый диалог»,
подсказка-вопрос, список ассистентов, две кнопки внизу.
**Что в карточке ассистента:**
- значок — цвет или картинка (как настроено в настройках агентов);
- имя;
- адрес сервера;
- состояние связи: «на связи» зелёным, «не отвечает» красным;
- кружок отметки справа.
**Поведение:**
- **первый отмечен сразу** — создать диалог можно одним нажатием;
- выбран ассистент без связи → под списком появляется предупреждение,
но выбор не запрещается (связь может пропасть на секунду);
- окно маленькое и без вкладок: одно действие — один выбор.
**Когда этой модалки не должно быть:**
- **ассистент один** — диалог создаётся сразу в нём, спрашивать не о чем;
- **ассистентов ноль** — кнопки «+» нет вообще (создавать диалог не в ком),
вместо списка диалогов подсказка «добавьте ассистента в настройках».
**Отвергнуто при одобрении:** пояснительная панель над окном клиента — она
попала в макет случайно, это была заметка для проверки, а не часть интерфейса.
В одобренном файле её нет и быть не должно.
---
## 004-settings-agents.html — Настройки агентов
**Два окна друг над другом.** Первое — настройки (620 px): список агентов и форма
нового. Второе, поверх — проверка связи (480 px).
**Окно 1, список агентов.** У каждого: знак (цвет или картинка), имя, адрес,
состояние — «на связи» зелёным, «не отвечает» красным, «не проверялся» серым.
Кнопки в строке: **проверить связь (⇄)**, **изменить (✎)**, **удалить (🗑)**.
Внизу пунктирная «+ Добавить агента».
**Окно 1, форма нового** — три поля:
- **Название.** Пояснение: имя придумывает пользователь, сервер своего имени
не сообщает (это из исходников, не догадка).
- **Адрес сервера** — целиком, вместе с путём `/agentik`. Прямо в строке адреса
кнопка «Проверить связь».
- **Как отличать в списке** — вкладки **Цвет / Картинка**, выбирается что-то одно.
Цвет — из пяти. Картинка — файл с диска, круглый превью, «Выбрать файл…» и «Убрать».
⚠️ Картинка **копируется внутрь клиента**, не берётся по исходному адресу.
**Окно 2, проверка связи — отдельное окно поверх настроек**, три состояния
в одном окне: идёт (крутится кольцо) → отвечает (зелёная галочка, подробности:
ответ, адрес, число диалогов, отклик) → не отвечает (красный крестик, подсказка
что проверить). Внизу всегда «Проверить снова». Кнопка «Добавить агента»
доступна только при успехе. Работает и для добавленных агентов — кнопкой «⇄».
**Почему проверка отдельным окном:** места больше, основную форму не дёргает,
одно окно годится и для новой формы, и для уже добавленного агента.
**Честно о непроверенном:** живого агента на момент макета не поднято — на порту
8080 отвечал чужой сервер. Адреса запросов взяты из исходников (`GET /agentik/health`
и список диалогов, оба ничего не меняют), но замеры времени и число диалогов
в макете условные. На настоящем агенте проверка ещё не гонялась.
## 006-groups-manage/ — Управление группами
**Четыре файла, по одному состоянию в каждом.** Интерактивности нет намеренно:
вместо неё отдельный файл на каждое состояние.
| Файл | Что показывает |
|---|---|
| `1-list.html` | список групп: «изменить» и «удалить» у каждой, внизу «+ Добавить группу» |
| `2-add.html` | новая группа: только название |
| `3-edit.html` | изменение: переименование, удаление отдельным блоком «ОПАСНОЕ» |
| `4-delete.html` | подтверждение: диалоги не удаляются, вернутся в «Все чаты» |
**Как открывается:** кнопкой-карандашом (✎) в ленте групп над списком диалогов —
справа, отдельно от чипов. Прежний «+» на этом месте убран: рядом стоят чипы
самих групп, и всё похожее на «+» читается как «добавить группу», хотя диалог
создаётся кнопкой в шапке списка.
**Решения внутри:**
- **«Все чаты» — встроенная группа**, не удаляется и не переименовывается
(пунктирная рамка, пометка «встроенная»). Нужна как надёжное место, где виден
весь список.
- **Удаление группы диалоги не удаляет** — в подтверждении это сказано первым
делом. Диалоги останутся и будут видны в «Все чаты», «корзины» у них нет.
- **У группы только название.** Цвет, значок, порядок — не делаем, пока не попросили.
- **Пустая группа допустима** — можно создать заранее, диалоги разложить потом.
- **Подсказка при создании:** диалоги в группу попадают позже, из самого диалога.
## Про цвета в макетах
Цвета в макетах стоят числами — так и надо, иначе макет не будет выглядеть
задуманным. **В коде так нельзя:** цвет берётся из цветовой схемы по смыслу,
потому что тем будет несколько. Разбор — `THEMES.md`.
## Как смотреть
Открыть файл в браузере — самодостаточный, без сборки и без зависимостей.
- в `001` переключается узкий/широкий вид и видны состояния сообщений;
- в `004` переключаются список/форма, вкладки Цвет/Картинка; кнопкой «⇄» или
«Проверить связь» открывается окно проверки (оба состояния — успех и отказ);
- в `005` кликом по карточке меняется выбор и появляется предупреждение;
- `006` — четыре файла, открывать по одному, по состоянию.
-25
View File
@@ -1,25 +0,0 @@
# Вариант 2 — Rail / Voice-first
## Суть
Голос — главное действие, а не дополнение к клавиатуре. Огромная кнопка микрофона
слева в панели ввода, живая расшифровка на экране, пока говоришь.
## Ключевые решения
- **Раскладка:** рейка иконок (76) + диалоги (296) + чат. Рейка — вход в память агента
и инструменты, это не просто мессенджер.
- **Голос:** кнопка 58 px, при записи краснеет и пульсирует. Над полем раскрывается
панель: волна, таймер и **текст, который появляется по мере речи**.
- **Три выхода из записи:** «Отправить» / «В поле ввода» (поправить руками) / «Отменить».
Голосовое сообщение в ленте рисуется с волной и кнопкой проигрывания.
- **Горячие клавиши:** Ctrl+R — запись, удержание пробела — говорить, Esc — отмена.
## Что хорошо
- Голосом пользоваться реально удобно: видно, что тебя услышали, до отправки.
- Ошибку в тексте видно до отправки — можно поправить.
## Что слабо
- Больше элементов на экране, надо разбираться.
- «Пробел на удержание» конфликтует с обычным набором текста — нужна аккуратность.
## Кому
Тому, кто в основном диктует, а не печатает.
-342
View File
@@ -1,342 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Вариант 2 — Rail / Voice-first</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0E1418; --panel: #141E24; --panel2: #1B2830; --line: #22323C;
--fg: #E8EEF2; --dim: #7A93A3; --accent: #4FD1C5; --hot: #FF6B6B;
--bot-bubble: #1B2830; --user-bubble: #1F4B54;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #070B0D; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
.window {
width: 1240px; height: 750px; background: var(--bg); border-radius: 14px; display: flex;
overflow: hidden; border: 1px solid var(--line); box-shadow: 0 24px 80px rgba(0,0,0,.65);
}
/* ---- узкая колонка-рейка: чаты иконками ---- */
.rail {
width: 76px; flex: 0 0 76px; background: var(--panel); border-right: 1px solid var(--line);
display: flex; flex-direction: column; align-items: center; padding: 12px 0; gap: 10px;
}
.rail-btn {
width: 46px; height: 46px; border-radius: 13px; border: none; background: var(--panel2);
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px;
position: relative; transition: .15s;
}
.rail-btn:hover { color: var(--fg); }
.rail-btn.active { background: var(--accent); color: #07201D; }
.rail-btn .dot-badge {
position: absolute; top: -3px; right: -3px; background: var(--accent); color: #07201D;
font-size: 10px; font-weight: 800; border-radius: 8px; padding: 1px 5px; min-width: 16px;
}
.rail-btn .label {
position: absolute; bottom: -14px; font-size: 9px; color: var(--dim); white-space: nowrap;
}
.rail-sep { width: 28px; height: 1px; background: var(--line); margin: 8px 0; }
.rail-spacer { flex: 1; }
/* ---- средний столбец: список диалогов ---- */
.list { width: 296px; flex: 0 0 296px; background: var(--panel); border-right: 1px solid var(--line);
display: flex; flex-direction: column; }
.l-head { padding: 16px 18px 12px; }
.l-head h1 { font-size: 16.5px; font-weight: 650; letter-spacing: .2px; }
.l-head p { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.search {
margin: 0 18px 10px; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13px; outline: none;
}
.search:focus { border-color: var(--accent); }
.search::placeholder { color: var(--dim); }
.rows { flex: 1; overflow-y: auto; padding: 0 10px 12px; }
.row {
display: flex; gap: 11px; padding: 10px 11px; border-radius: 11px; cursor: pointer;
border-left: 2px solid transparent;
}
.row:hover { background: var(--panel2); }
.row.active { background: var(--panel2); border-left-color: var(--accent); }
.row .av {
width: 38px; height: 38px; flex: 0 0 38px; border-radius: 11px; display: grid; place-items: center;
font-size: 13px; font-weight: 700; background: rgba(79,209,197,.16); color: var(--accent);
}
.row .mid { flex: 1; min-width: 0; }
.row .nm { font-size: 13.5px; font-weight: 550; display: flex; justify-content: space-between; }
.row .tm { font-size: 10.5px; color: var(--dim); font-weight: 400; }
.row .pv { font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin-top: 2px; }
/* ---- основная зона чата ---- */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.m-head { height: 60px; flex: 0 0 60px; display: flex; align-items: center; gap: 12px;
padding: 0 22px; border-bottom: 1px solid var(--line); }
.m-head .t { font-size: 14.5px; font-weight: 600; }
.m-head .s { font-size: 11.5px; color: var(--dim); }
.ghost { margin-left: auto; display: flex; gap: 6px; }
.ghost button { background: transparent; border: 1px solid var(--line); color: var(--dim);
border-radius: 8px; padding: 5px 11px; font-size: 11.5px; cursor: pointer; }
.ghost button:hover { color: var(--fg); border-color: var(--dim); }
.thread { flex: 1; overflow-y: auto; padding: 22px 26px; display: flex; flex-direction: column; gap: 13px; }
.b { max-width: 76%; padding: 10px 14px; border-radius: 14px; font-size: 13.5px; }
.b.u { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.b.a { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
.b .tm { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
/* голосовое сообщение — с волной прямо в пузыре */
.voice-b {
align-self: flex-end; background: var(--user-bubble); border-radius: 14px 14px 4px 14px;
padding: 10px 14px; display: flex; align-items: center; gap: 11px; min-width: 250px;
}
.play { width: 30px; height: 30px; border-radius: 50%; background: var(--accent); color: #07201D;
border: none; display: grid; place-items: center; cursor: pointer; font-size: 12px; }
.vwave { flex: 1; display: flex; align-items: center; gap: 2px; height: 24px; }
.vwave i { flex: 1; background: rgba(232,238,242,.5); border-radius: 2px; }
.vdur { font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }
/* ---- КОМПОЗЕР: голос — главный ---- */
.composer { border-top: 1px solid var(--line); background: var(--panel); padding: 16px 22px 14px; }
.mic-zone { display: flex; align-items: center; gap: 16px; }
.mic-big {
width: 58px; height: 58px; flex: 0 0 58px; border-radius: 50%; border: none; cursor: pointer;
background: var(--accent); color: #07201D; font-size: 23px; display: grid; place-items: center;
transition: .2s;
}
.mic-big:hover { transform: scale(1.05); }
.mic-big.on { background: var(--hot); color: #fff; animation: ring 1.5s infinite; }
@keyframes ring { 0%,100% { box-shadow: 0 0 0 0 rgba(255,107,107,.6); } 50% { box-shadow: 0 0 0 13px rgba(255,107,107,0); } }
.mic-side { flex: 1; min-width: 0; }
.mic-hint { font-size: 12px; color: var(--dim); margin-bottom: 7px; }
.mic-hint b { color: var(--fg); font-weight: 550; }
.type-row { display: flex; align-items: center; gap: 9px; }
.type-row textarea {
flex: 1; background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 10px 13px;
color: var(--fg); font-family: inherit; font-size: 13.5px; outline: none; resize: none;
height: 40px; max-height: 110px; line-height: 1.4;
}
.type-row textarea:focus { border-color: var(--accent); }
.type-row textarea::placeholder { color: var(--dim); }
.go { height: 40px; padding: 0 17px; border-radius: 10px; border: 1px solid var(--line);
background: transparent; color: var(--dim); font-size: 12.5px; cursor: pointer; }
.go.active { background: var(--accent); border-color: var(--accent); color: #07201D; font-weight: 650; }
/* расшифровка в реальном времени */
.live-asr { margin-top: 11px; display: none; background: var(--bg); border: 1px solid var(--hot);
border-radius: 11px; padding: 11px 14px; }
.live-asr.on { display: block; }
.live-asr .top { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; font-size: 11.5px; color: var(--hot); font-weight: 600; }
.blink { width: 7px; height: 7px; border-radius: 50%; background: var(--hot); animation: bl .9s infinite; }
@keyframes bl { 50% { opacity: .15; } }
.big-wave { display: flex; align-items: center; gap: 3px; height: 30px; margin-bottom: 8px; }
.big-wave i { width: 3px; background: var(--accent); border-radius: 2px; height: 25%; animation: bw .8s infinite ease-in-out; }
.big-wave i:nth-child(2n) { animation-delay: .1s; } .big-wave i:nth-child(3n) { animation-delay: .2s; }
@keyframes bw { 0%,100% { height: 18%; } 50% { height: 100%; } }
.asr-line { font-size: 14px; color: var(--fg); min-height: 20px; }
.asr-line .cursor { display: inline-block; width: 2px; height: 15px; background: var(--accent);
vertical-align: middle; animation: bl .8s infinite; }
.live-actions { display: flex; gap: 8px; margin-top: 11px; }
.live-actions button { border-radius: 9px; padding: 7px 14px; font-size: 12.5px; cursor: pointer; border: 1px solid var(--line); background: transparent; color: var(--dim); }
.live-actions .prim { background: var(--accent); border-color: var(--accent); color: #07201D; font-weight: 620; }
.live-actions .dang { color: var(--hot); }
.foot-note { font-size: 10.5px; color: var(--dim); margin-top: 9px; }
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
/* окно целиком влезает на любой экран — не режем картинку */
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
</style>
</head>
<body>
<div class="window">
<!-- ============ РЕЙКА ============ -->
<nav class="rail">
<button class="rail-btn active" title="Диалоги">💬<span class="dot-badge">3</span></button>
<button class="rail-btn" title="Память агента">🧠</button>
<button class="rail-btn" title="Инструменты">⚙</button>
<div class="rail-sep"></div>
<button class="rail-btn" title="Новый диалог">+</button>
<div class="rail-spacer"></div>
<button class="rail-btn" title="Настройки">⚙︎</button>
</nav>
<!-- ============ СПИСОК ДИАЛОГОВ ============ -->
<section class="list">
<div class="l-head">
<h1>Диалоги</h1>
<p>локальный агент · 3 непрочитанных</p>
</div>
<input class="search" placeholder="Поиск…">
<div class="rows">
<div class="row active">
<div class="av">АГ</div>
<div class="mid">
<div class="nm"><span>agentik</span><span class="tm">14:22</span></div>
<div class="pv">Собрал отчёт, жду правок</div>
</div>
</div>
<div class="row">
<div class="av">ОЧ</div>
<div class="mid">
<div class="nm"><span>Отчёт за квартал</span><span class="tm">12:05</span></div>
<div class="pv">Файл лежит на диске</div>
</div>
</div>
<div class="row">
<div class="av">РЗ</div>
<div class="mid">
<div class="nm"><span>Разбор логов</span><span class="tm">вчера</span></div>
<div class="pv">Нашёл 3 ошибки 502</div>
</div>
</div>
<div class="row">
<div class="av">ПО</div>
<div class="mid">
<div class="nm"><span>Solana-бэкенд</span><span class="tm">вчера</span></div>
<div class="pv">Девайс-нет стартовал</div>
</div>
</div>
<div class="row">
<div class="av">КН</div>
<div class="mid">
<div class="nm"><span>Книги</span><span class="tm">пн</span></div>
<div class="pv">Разложил по полкам</div>
</div>
</div>
</div>
</section>
<!-- ============ ЧАТ ============ -->
<main class="main">
<header class="m-head">
<div>
<div class="t">agentik</div>
<div class="s">на связи · codding-big</div>
</div>
<div class="ghost">
<button>Стоп</button>
<button>Очистить</button>
</div>
</header>
<div class="thread" id="thread">
<div class="b u">Посмотри логи за вчера<span class="tm">14:10</span></div>
<div class="b a">Проверил: 3 ошибки 502 около 04:20, все — обрыв соединения с upstream. Виновник — таймаут прокси. Поправить?<span class="tm">14:12</span></div>
<div class="voice-b">
<button class="play" onclick="togglePlay(this)">▶</button>
<div class="vwave" id="pastWave"></div>
<span class="vdur">0:07</span>
</div>
<div class="b a">Понял: увеличить таймаут до 60 секунд и перезапустить прокси. Сделал — прокси поднят заново, ошибок за последний час нет.<span class="tm">14:20</span></div>
</div>
<div class="composer">
<div class="live-asr" id="liveAsr">
<div class="top"><span class="blink"></span> ЗАПИСЬ · <span id="liveTime">0:00</span></div>
<div class="big-wave" id="bigWave"></div>
<div class="asr-line"><span id="asrLine"></span><span class="cursor"></span></div>
<div class="live-actions">
<button class="prim" onclick="stopRec(false)">Отправить</button>
<button onclick="insertAsText()">В поле ввода</button>
<button class="dang" onclick="stopRec(true)">Отменить</button>
</div>
</div>
<div class="mic-zone">
<button class="mic-big" id="micBtn" title="Запись (Ctrl+R, удержание пробела)">🎙</button>
<div class="mic-side">
<div class="mic-hint"><b>Говорите</b> — текст появится в поле ниже. Можно править перед отправкой.</div>
<div class="type-row">
<textarea id="ta" placeholder="Или напишите руками…"></textarea>
<button class="go" id="goBtn" disabled>Отправить</button>
</div>
</div>
</div>
<div class="foot-note">
<kbd>Ctrl</kbd>+<kbd>R</kbd> — начать/стоп · <kbd>Пробел</kbd> (удержание) — говорить · <kbd>Enter</kbd> — отправить · <kbd>Esc</kbd> — отменить запись
</div>
</div>
</main>
</div>
<script>
const ta = document.getElementById('ta'), goBtn = document.getElementById('goBtn');
const micBtn = document.getElementById('micBtn'), liveAsr = document.getElementById('liveAsr');
const thread = document.getElementById('thread');
document.getElementById('pastWave').innerHTML =
Array.from({length: 34}, () => '<i style="height:' + (18 + Math.random()*80) + '%"></i>').join('');
document.getElementById('bigWave').innerHTML = Array.from({length: 40}, () => '<i></i>').join('');
ta.addEventListener('input', () => {
goBtn.disabled = ta.value.trim() === '';
goBtn.classList.toggle('active', ta.value.trim() !== '');
ta.style.height = '40px'; ta.style.height = Math.min(ta.scrollHeight, 110) + 'px';
});
function send() {
const t = ta.value.trim(); if (!t) return;
add(t, 'u'); ta.value = ''; ta.style.height = '40px';
goBtn.disabled = true; goBtn.classList.remove('active');
setTimeout(() => add('Выполняю: «' + t + '».', 'a'), 800);
}
function add(text, who) {
const d = document.createElement('div'); d.className = 'b ' + who;
const n = new Date();
d.innerHTML = text + '<span class="tm">' + n.getHours() + ':' + String(n.getMinutes()).padStart(2,'0') + '</span>';
thread.appendChild(d); thread.scrollTop = thread.scrollHeight;
}
goBtn.onclick = send;
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
// ---- запись с живой расшифровкой ----
const PHRASE = 'посмотри логи за вчерашний день и скажи что там с ошибками';
let tmr = null, sec = 0, wordIdx = 0;
function startRec() {
sec = 0; wordIdx = 0;
liveAsr.classList.add('on'); micBtn.classList.add('on');
document.getElementById('asrLine').textContent = '';
tmr = setInterval(() => {
sec++;
document.getElementById('liveTime').textContent = '0:' + String(sec).padStart(2,'0');
const words = PHRASE.split(' ');
if (wordIdx < words.length) {
document.getElementById('asrLine').textContent = words.slice(0, ++wordIdx).join(' ');
}
}, 450);
}
function stopRec(cancel) {
clearInterval(tmr);
liveAsr.classList.remove('on'); micBtn.classList.remove('on');
if (cancel) return;
const text = document.getElementById('asrLine').textContent;
if (!text) return;
if (sec >= 2) { // короткие — сразу в чат, длинные — в поле
add(text, 'u');
setTimeout(() => add('Записал голосом, разбираю…', 'a'), 700);
}
}
function insertAsText() {
ta.value = (ta.value ? ta.value + ' ' : '') + document.getElementById('asrLine').textContent;
ta.dispatchEvent(new Event('input')); ta.focus(); stopRec(true);
}
function togglePlay(btn) { btn.textContent = btn.textContent === '▶' ? '❚❚' : '▶'; }
micBtn.onclick = () => liveAsr.classList.contains('on') ? stopRec(false) : startRec();
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); micBtn.click(); }
if (e.key === 'Escape' && liveAsr.classList.contains('on')) stopRec(true);
});
</script>
</body>
</html>
-25
View File
@@ -1,25 +0,0 @@
# Вариант 3 — Three-pane / Command center
## Суть
Три панели: диалоги, чат и **состояние агента**. Работа агента видна не как текст
в переписке, а как отдельные раскрывающиеся шаги.
## Ключевые решения
- **Раскладка:** 268 + центр + 306. Третья панель — модель, контекст, инструменты,
память агента, файлы сессии.
- **Шаг агента** — сворачиваемый блок: что делал, сколько заняло, детали по клику.
Это то, чем агент отличается от собеседника, и здесь оно вынесено в структуру.
- **Кнопка записи:** компактная рядом с «Отправить»; при записи снизу появляется
строка: квадрат «стоп», индикатор уровня, текущая расшифровка, таймер.
- **Короткая диктовка** (от 2 с) уходит сразу, текст остаётся в поле.
## Что хорошо
- Видно, что агент делает прямо сейчас и сколько это стоит.
- Удобно на широком экране, ничего не надо прятать.
## Что слабо
- На узком экране (ноутбук 1366) третью панель придётся убирать.
- Настройки сессии — не то, на что хочется смотреть постоянно.
## Кому
Тому, кто держит агента включённым весь день и следит за расходом.
-345
View File
@@ -1,345 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Вариант 3 — Three-pane / Command center</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0F1115; --panel: #161A21; --panel2: #1E242D; --panel3: #252C37; --line: #2A323D;
--fg: #ECEFF3; --dim: #808FA0; --accent: #7C8CF8; --ok: #4ADE80; --warn: #FBBF24; --hot: #F87171;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #06080A; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 22px;
}
.win { width: 1360px; height: 780px; background: var(--bg); border-radius: 13px; overflow: hidden;
border: 1px solid var(--line); display: flex; flex-direction: column; box-shadow: 0 26px 90px rgba(0,0,0,.7); }
/* ---- строка меню-заголовка ---- */
.titlebar { height: 42px; flex: 0 0 42px; display: flex; align-items: center; gap: 12px;
padding: 0 16px; background: var(--panel); border-bottom: 1px solid var(--line); }
.lights { display: flex; gap: 7px; }
.lights i { width: 11px; height: 11px; border-radius: 50%; }
.lights i:nth-child(1) { background: #FF5F57; } .lights i:nth-child(2) { background: #FEBC2E; }
.lights i:nth-child(3) { background: #28C840; }
.tb-title { font-size: 12.5px; color: var(--dim); }
.tb-title b { color: var(--fg); font-weight: 550; }
.tb-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--dim); }
.pill { background: var(--panel2); border: 1px solid var(--line); border-radius: 20px; padding: 3px 11px;
display: flex; align-items: center; gap: 6px; }
.pill .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.body { flex: 1; display: flex; min-height: 0; }
/* ========== ПАНЕЛЬ 1: чаты ========== */
.p1 { width: 268px; flex: 0 0 268px; background: var(--panel); border-right: 1px solid var(--line);
display: flex; flex-direction: column; }
.p-head { padding: 14px 15px 9px; display: flex; align-items: center; justify-content: space-between; }
.p-head h2 { font-size: 11px; letter-spacing: 1.3px; text-transform: uppercase; color: var(--dim); font-weight: 650; }
.p-head button { background: var(--accent); border: none; color: #fff; border-radius: 7px;
width: 24px; height: 24px; cursor: pointer; font-size: 15px; line-height: 1; }
.f-tabs { display: flex; gap: 4px; padding: 0 12px 10px; flex-wrap: wrap; }
.f-tab { font-size: 11px; padding: 4px 9px; border-radius: 7px; color: var(--dim); background: var(--panel2);
cursor: pointer; border: 1px solid transparent; }
.f-tab.on { color: var(--fg); border-color: var(--accent); background: rgba(124,140,248,.14); }
.q { margin: 0 12px 9px; padding: 7px 11px; border-radius: 8px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 12.5px; outline: none; }
.q:focus { border-color: var(--accent); } .q::placeholder { color: var(--dim); }
.cf { flex: 1; overflow-y: auto; padding: 0 8px 12px; }
.c { display: flex; gap: 10px; padding: 9px 10px; border-radius: 9px; cursor: pointer; }
.c:hover { background: var(--panel2); }
.c.on { background: var(--panel2); box-shadow: inset 2px 0 0 var(--accent); }
.c .a { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 9px; display: grid; place-items: center;
font-size: 11.5px; font-weight: 700; background: var(--panel3); color: var(--accent); }
.c .m { flex: 1; min-width: 0; }
.c .n { font-size: 13px; font-weight: 550; display: flex; justify-content: space-between; gap: 6px; }
.c .n span.t { font-size: 10px; color: var(--dim); font-weight: 400; }
.c .p { font-size: 11.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.c.new .n { color: var(--accent); }
/* ========== ПАНЕЛЬ 2: диалог ========== */
.p2 { flex: 1; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--line); }
.p2h { height: 54px; flex: 0 0 54px; display: flex; align-items: center; gap: 11px; padding: 0 18px;
border-bottom: 1px solid var(--line); }
.p2h .nm { font-size: 14px; font-weight: 600; }
.p2h .sub { font-size: 11px; color: var(--dim); }
.p2h .acts { margin-left: auto; display: flex; gap: 6px; }
.p2h .acts button { background: var(--panel2); border: 1px solid var(--line); color: var(--dim);
border-radius: 7px; padding: 5px 11px; font-size: 11.5px; cursor: pointer; }
.p2h .acts button:hover { color: var(--fg); }
.flow { flex: 1; overflow-y: auto; padding: 18px 22px; display: flex; flex-direction: column; gap: 11px; }
.u { align-self: flex-end; max-width: 78%; background: #2A3350; padding: 9px 13px; border-radius: 12px 12px 3px 12px;
font-size: 13.5px; }
.a { align-self: flex-start; max-width: 78%; background: var(--panel2); padding: 9px 13px;
border-radius: 12px 12px 12px 3px; font-size: 13.5px; }
.tm { font-size: 10px; color: var(--dim); margin-top: 4px; }
/* свёрнутый шаг работы агента — то, что отличает агента от чата */
.step { align-self: flex-start; width: 100%; max-width: 92%; background: var(--panel);
border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.step-h { display: flex; align-items: center; gap: 9px; padding: 8px 12px; cursor: pointer; font-size: 12.5px; }
.step-h:hover { background: var(--panel2); }
.step-h .ico { color: var(--accent); }
.step-h .nm { color: var(--fg); font-weight: 550; }
.step-h .stat { margin-left: auto; font-size: 10.5px; color: var(--ok); }
.step-h .chev { color: var(--dim); font-size: 10px; }
.step-b { display: none; padding: 0 12px 11px 34px; font-size: 12px; color: var(--dim); }
.step.open .step-b { display: block; }
.step-b code { color: #A8B6C8; background: var(--bg); border: 1px solid var(--line); border-radius: 5px;
padding: 1px 6px; font-size: 11px; }
/* ========== ПАНЕЛЬ 3: состояние агента ========== */
.p3 { width: 306px; flex: 0 0 306px; background: var(--panel); display: flex; flex-direction: column; }
.sec { border-bottom: 1px solid var(--line); padding: 13px 15px; }
.sec h3 { font-size: 10.5px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--dim);
font-weight: 650; margin-bottom: 10px; display: flex; align-items: center; gap: 7px; }
.kv { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 7px; }
.kv span:first-child { color: var(--dim); }
.kv span:last-child { font-weight: 550; }
.bar { height: 5px; border-radius: 3px; background: var(--panel3); overflow: hidden; margin-top: 4px; }
.bar i { display: block; height: 100%; background: var(--accent); }
.tool-chip { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px 0; }
.tool-chip .s { width: 7px; height: 7px; border-radius: 50%; }
.tool-chip .s.run { background: var(--warn); animation: p 1s infinite; }
.tool-chip .s.ok { background: var(--ok); }
@keyframes p { 50% { opacity: .3; } }
.mem { font-size: 11.5px; color: var(--dim); padding: 7px 9px; background: var(--panel2);
border-radius: 8px; margin-bottom: 6px; line-height: 1.4; }
.mem b { color: var(--fg); font-weight: 550; display: block; font-size: 11.5px; margin-bottom: 2px; }
/* ========== КОМПОЗЕР (низ панели 2) ========== */
.comp { border-top: 1px solid var(--line); background: var(--panel); padding: 13px 18px 14px; }
.rec-state { display: none; align-items: center; gap: 12px; background: var(--bg); border: 1px solid var(--hot);
border-radius: 10px; padding: 10px 13px; margin-bottom: 10px; }
.rec-state.on { display: flex; }
.stop-rec { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 50%; background: var(--hot);
border: none; color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 12px; }
.sq { width: 11px; height: 11px; background: #fff; border-radius: 2px; }
.meter { flex: 1; display: flex; align-items: center; gap: 3px; height: 26px; }
.meter i { width: 3px; background: var(--hot); border-radius: 2px; height: 25%; animation: mn .9s infinite ease-in-out; }
.meter i:nth-child(2n) { animation-delay: .12s; } .meter i:nth-child(3n) { animation-delay: .24s; }
@keyframes mn { 0%,100% { height: 20%; } 50% { height: 100%; } }
.rt { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--hot); font-weight: 600; }
.rec-state .live-txt { font-size: 11.5px; color: var(--dim); max-width: 260px; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; }
.crow { display: flex; align-items: flex-end; gap: 8px; }
.cbox { flex: 1; display: flex; align-items: flex-end; background: var(--bg); border: 1px solid var(--line);
border-radius: 10px; padding: 9px 12px; }
.cbox:focus-within { border-color: var(--accent); }
.cbox textarea { flex: 1; background: transparent; border: none; outline: none; color: var(--fg);
font-family: inherit; font-size: 13.5px; resize: none; height: 21px; max-height: 110px; line-height: 1.45; }
.cbox textarea::placeholder { color: var(--dim); }
.ic { width: 36px; height: 36px; border-radius: 9px; border: 1px solid var(--line); background: var(--bg);
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 15px; flex: 0 0 36px; }
.ic:hover { color: var(--fg); }
.ic.rec { border-color: var(--accent); color: var(--accent); }
.ic.rec.live { background: var(--hot); border-color: var(--hot); color: #fff; animation: rp 1.4s infinite; }
@keyframes rp { 0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,.5); } 50% { box-shadow: 0 0 0 9px rgba(248,113,113,0); } }
.send { height: 36px; padding: 0 16px; border-radius: 9px; border: none; background: var(--accent);
color: #fff; font-size: 12.5px; font-weight: 600; cursor: pointer; }
.send:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
.keys { font-size: 10.5px; color: var(--dim); margin-top: 9px; display: flex; gap: 14px; }
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
/* окно целиком влезает на любой экран — не режем картинку */
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
</style>
</head>
<body>
<div class="win">
<div class="titlebar">
<div class="lights"><i></i><i></i><i></i></div>
<div class="tb-title">agentik-desktop — <b>agentik · рабочий</b></div>
<div class="tb-right">
<span class="pill"><span class="d"></span> агент на связи</span>
<span>Память: 128 заметок</span>
</div>
</div>
<div class="body">
<!-- ======== ПАНЕЛЬ 1 ======== -->
<aside class="p1">
<div class="p-head"><h2>Диалоги</h2><button title="Новый">+</button></div>
<div class="f-tabs">
<div class="f-tab on">Все</div><div class="f-tab">Работа</div>
<div class="f-tab">Инфра</div><div class="f-tab">Дом</div><div class="f-tab">+</div>
</div>
<input class="q" placeholder="Поиск диалога…">
<div class="cf">
<div class="c on"><div class="a">АГ</div><div class="m">
<div class="n"><span>agentik · рабочий</span><span class="t">14:22</span></div>
<div class="p">Собрал отчёт, жду правок</div></div></div>
<div class="c new"><div class="a">ОЧ</div><div class="m">
<div class="n"><span>Отчёт за квартал</span><span class="t">12:05</span></div>
<div class="p">Файл лежит на диске</div></div></div>
<div class="c"><div class="a">РЗ</div><div class="m">
<div class="n"><span>Разбор логов nginx</span><span class="t">вчера</span></div>
<div class="p">Нашёл 3 ошибки 502</div></div></div>
<div class="c"><div class="a">ПО</div><div class="m">
<div class="n"><span>Solana-бэкенд</span><span class="t">вчера</span></div>
<div class="p">Девайс-нет стартовал</div></div></div>
<div class="c"><div class="a">КН</div><div class="m">
<div class="n"><span>Книги</span><span class="t">пн</span></div>
<div class="p">Разложил по полкам</div></div></div>
</div>
</aside>
<!-- ======== ПАНЕЛЬ 2 ======== -->
<section class="p2">
<div class="p2h">
<div>
<div class="nm">agentik · рабочий</div>
<div class="sub">codding-big · контекст 34%</div>
</div>
<div class="acts"><button>Стоп</button><button>Очистить</button></div>
</div>
<div class="flow" id="flow">
<div class="u">Разберись, почему CI красный, и выпусти релиз 3<span class="tm">14:05</span></div>
<div class="step open" onclick="this.classList.toggle('open')">
<div class="step-h"><span class="ico">🔍</span><span class="nm">Поиск причины</span>
<span class="stat">готово · 4 с</span><span class="chev">▼</span></div>
<div class="step-b">Нашёл: в именах тестов длинное тире, сборка идёт в ASCII.<br>
<code>InvalidPathException: unmappable characters</code></div>
</div>
<div class="step" onclick="this.classList.toggle('open')">
<div class="step-h"><span class="ico">✎</span><span class="nm">Правка 16 имён тестов</span>
<span class="stat">готово · 12 с</span><span class="chev">▼</span></div>
<div class="step-b">Заменено на обычный дефис в 4 файлах. Ничего другого не тронуто.</div>
</div>
<div class="step" onclick="this.classList.toggle('open')">
<div class="step-h"><span class="ico">⚙</span><span class="nm">Сборка и проверка</span>
<span class="stat">готово · 1 м 10 с</span><span class="chev">▼</span></div>
<div class="step-b">14 модулей, тесты зелёные, jar-ы собраны.</div>
</div>
<div class="a">Готово. CI зелёный, релиз «3» опубликован: 101 артефакт в Nexus.<span class="tm">14:22</span></div>
</div>
<div class="comp">
<div class="rec-state" id="recState">
<button class="stop-rec" onclick="stopRec(false)"><span class="sq"></span></button>
<div class="meter" id="meter"></div>
<span class="live-txt" id="liveTxt">слушаю…</span>
<span class="rt" id="rt">0:00</span>
<button class="ic" onclick="stopRec(true)" title="Отменить">✕</button>
</div>
<div class="crow">
<div class="cbox"><textarea id="ta" placeholder="Сообщение агенту…"></textarea></div>
<button class="ic rec" id="recBtn" title="Голосовое (Ctrl+R)">🎙</button>
<button class="send" id="sendBtn" disabled>Отправить</button>
</div>
<div class="keys">
<span><kbd>Ctrl</kbd>+<kbd>R</kbd> запись</span>
<span><kbd>Enter</kbd> отправить</span>
<span><kbd>Shift</kbd>+<kbd>Enter</kbd> перенос</span>
</div>
</div>
</section>
<!-- ======== ПАНЕЛЬ 3 ======== -->
<aside class="p3">
<div class="sec">
<h3>Сессия</h3>
<div class="kv"><span>Модель</span><span>codding-big</span></div>
<div class="kv"><span>Контекст</span><span>34 %</span></div>
<div class="bar"><i style="width:34%"></i></div>
<div class="kv" style="margin-top:9px"><span>Ходов</span><span>18</span></div>
<div class="kv"><span>Стоимость</span><span>0.42 ₽</span></div>
</div>
<div class="sec">
<h3>Инструменты</h3>
<div class="tool-chip"><span class="s ok"></span> shell · <span style="color:var(--dim)">готов</span></div>
<div class="tool-chip"><span class="s ok"></span> read_file · <span style="color:var(--dim)">готов</span></div>
<div class="tool-chip"><span class="s run"></span> write_file · <span style="color:var(--dim)">работает</span></div>
<div class="tool-chip"><span class="s ok"></span> memory · <span style="color:var(--dim)">готов</span></div>
</div>
<div class="sec">
<h3>Память агента</h3>
<div class="mem"><b>Про CI agentik</b>длинное тире в именах тестов ломает сборку в ASCII</div>
<div class="mem"><b>Про Nexus</b>публикация идёт через общий action devops/publish</div>
</div>
<div class="sec" style="border-bottom:none">
<h3>Файлы сессии</h3>
<div class="kv"><span>standalone-3.jar</span><span style="color:var(--dim)">240 МБ</span></div>
<div class="kv"><span>agentik-cli-3.jar</span><span style="color:var(--dim)">7.2 МБ</span></div>
</div>
</aside>
</div>
</div>
<script>
const ta = document.getElementById('ta'), sendBtn = document.getElementById('sendBtn');
const flow = document.getElementById('flow');
const recBtn = document.getElementById('recBtn'), recState = document.getElementById('recState');
const liveTxt = document.getElementById('liveTxt');
document.getElementById('meter').innerHTML = Array.from({length: 34}, () => '<i></i>').join('');
ta.addEventListener('input', () => {
sendBtn.disabled = ta.value.trim() === '';
ta.style.height = '21px'; ta.style.height = Math.min(ta.scrollHeight, 110) + 'px';
});
function send() {
const t = ta.value.trim(); if (!t) return;
add(t, 'u'); ta.value = ''; ta.style.height = '21px'; sendBtn.disabled = true;
setTimeout(() => {
const s = document.createElement('div'); s.className = 'step';
s.onclick = () => s.classList.toggle('open');
s.innerHTML = '<div class="step-h"><span class="ico">⚙</span><span class="nm">Выполняю</span>' +
'<span class="stat">идёт…</span><span class="chev">▼</span></div>' +
'<div class="step-b">Задача: ' + t + '</div>';
flow.appendChild(s);
setTimeout(() => {
s.querySelector('.stat').textContent = 'готово · 3 с';
s.querySelector('.stat').style.color = 'var(--ok)';
s.classList.add('open');
add('Сделано: ' + t, 'a');
}, 1400);
}, 300);
}
function add(text, who) {
const d = document.createElement('div'); d.className = who;
const n = new Date();
d.innerHTML = text + '<span class="tm">' + n.getHours() + ':' + String(n.getMinutes()).padStart(2,'0') + '</span>';
flow.appendChild(d); flow.scrollTop = flow.scrollHeight;
}
sendBtn.onclick = send;
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
const P = 'разберись с красным си ай и выпусти новый релиз'.split(' ');
let tm = null, sec = 0, wi = 0;
function startRec() {
sec = 0; wi = 0; recState.classList.add('on'); recBtn.classList.add('live');
tm = setInterval(() => {
sec++; document.getElementById('rt').textContent = '0:' + String(sec).padStart(2,'0');
if (wi < P.length) liveTxt.textContent = P.slice(0, ++wi).join(' ');
}, 500);
}
function stopRec(cancel) {
clearInterval(tm); recState.classList.remove('on'); recBtn.classList.remove('live');
if (cancel) return;
const t = liveTxt.textContent;
if (t && sec >= 2) { add(t, 'u'); setTimeout(() => add('Принято голосом, начинаю.', 'a'), 800); }
}
recBtn.onclick = () => recState.classList.contains('on') ? stopRec(false) : startRec();
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); recBtn.click(); }
if (e.key === 'Escape' && recState.classList.contains('on')) stopRec(true);
});
</script>
</body>
</html>
-73
View File
@@ -1,73 +0,0 @@
# Вариант 4 — Настройки: агенты
> **МАКЕТ ОДОБРЕН 2026-09-19.** Одобренная версия — `approved/004-settings-agents.html`
> (без панели-переключателя и без фона основного окна). Этот файл — черновик
> со всеми состояниями и заметками.
## Суть
Два окна друг над другом. Первое — настройки: список агентов и форма нового.
Второе, поверх — **проверка связи**: нажал «Проверить связь» и она идёт
в отдельном окне, а не строкой внутри формы.
## Окно 1: список агентов
- У каждого агента: имя, адрес, состояние — «на связи» зелёным, «не отвечает» серым,
ошибка красным. Видно сразу, а не спрятано в глубине.
- Кнопки в строке: **проверить связь** (⇄), **изменить** (✎), **удалить** (🗑).
Проверка доступна и для уже добавленного агента — из этого же списка.
- Внизу пунктирная «+ Добавить агента».
## Окно 1, вид 2: форма нового агента
- **Название** — с пояснением, что имя придумывает пользователь: сервер своего
имени не сообщает (это из исходников, не догадка).
- **Адрес сервера** — целиком, вместе с путём `/agentik`. Прямо в строке адреса
кнопка **«Проверить связь»**.
- **Чем отличать в списке: цвет или картинка.** Переключатель, выбирается что-то одно.
- **Цвет** — простой выбор из пяти.
- **Картинка** — файл с диска: круглый превью, «Выбрать файл…» и «Убрать».
Картинка показывается там же, где был бы цвет: в списке агентов, в диалогах,
в шапке чата.
- ⚠️ **Картинку копировать себе, а не брать по исходному адресу.** Выбранный файл
сохраняется внутрь клиента; дальше используется только эта копия. Если держать
ссылку на исходный путь, значок пропадёт, когда файл удалят, переименуют или
вынут флешку. То же для картинки по ссылке: скачать и положить себе.
## Окно 2: проверка связи (отдельное, поверх настроек)
Три состояния, сменяются в одном окне:
1. **Идёт** — крутящееся кольцо, «Спрашиваю агента…», под ним адрес.
2. **Отвечает** — зелёная галочка, «Агент отвечает». Подробности: ответ на запрос,
адрес, число диалогов, отклик в миллисекундах. Подсказка «Адрес рабочий».
Кнопка внизу — «Добавить агента» (доступна).
3. **Не отвечает** — красный крестик. Подробности: нет ответа, отклик истёк.
Подсказка: проверить адрес, что агент запущен, что порт открыт и путь `/agentik`
указан целиком. Кнопка внизу заблокирована — добавить нельзя.
Внизу окна всегда есть **«Проверить снова»**. Закрытие окна возвращает к настройкам,
а не выбрасывает из них.
## Почему проверка отдельным окном — лучше
- Места больше: видно и результат, и подробности, и объяснение, что делать дальше.
- Основную форму не дёргает: пока проверяешь — поля на месте, ничего не сбилось.
- Одно и то же окно годится и для формы, и для уже добавленного агента.
## На чём основана проверка (не выдумка)
У сервера агента есть ответ на `GET /agentik/health` (отдаёт «ok») и запрос
списка диалогов. Оба безопасные — ничего не меняют. Адреса взяты из исходников.
⚠️ **Живого агента сейчас не поднято** — на порту 8080 отвечает чужой сервер (404).
Поэтому на настоящем агенте проверка ещё не гонялась: в макете показаны все три
состояния, но замеры времени и число диалогов — условные.
## Фон: папки вернулись
В первом варианте над списком диалогов есть **папки** — «Все чаты», «Работа», «Дом»,
«+». В прошлой версии этого макета я их потерял. Теперь на фоне видны и папки,
и поиск, и значок непрочитанных — как в одобренном варианте 1.
## Плюс: список диалогов с двумя агентами
Диалоги сгруппированы по агентам, у группы цветная точка, у каждой строки цветная
полоска, цвет аватара совпадает с цветом агента из настроек.
## Что слабо / открытые вопросы
- Одного цвета для различения может оказаться мало.
- Где хранится список агентов — не решено.
- Пароля/ключа доступа у библиотеки нет — поля для него нет.
-625
View File
@@ -1,625 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Настройки: агенты</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
}
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
/* ============ фон: обычный клиент ============ */
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.search {
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.search::placeholder { color: var(--dim); }
/* ── папки: как в выбранном варианте 1 ── */
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; align-items: flex-start; cursor: pointer; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.row-tail { flex: 0 0 auto; }
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
border-radius: 10px; padding: 1px 7px; }
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.ic-btn { width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 16px; margin-left: auto; }
.ic-btn:hover { background: var(--panel2); color: var(--fg); }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
/* ============ ЗАТЕМНЕНИЕ (одно на оба окна) ============ */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; display: none; }
.scrim.on { display: block; }
.scrim.top { background: rgba(0,0,0,.42); z-index: 200; }
/* ============ ОКНО НАСТРОЕК ============ */
.layer { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; pointer-events: none; }
.layer.on { display: flex; }
.layer > * { pointer-events: auto; }
/* порядок слоёв: настройки 150, проверка поверх 250 */
#layerSettings { z-index: 150; }
#layerCheck { z-index: 250; }
.dialog {
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px; background: #17212B;
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* список агентов */
.agent-card {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.agent-card:hover { border-color: var(--line); }
.agent-card .avatar { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
.ag-mid { flex: 1; min-width: 0; }
.ag-name { font-size: 14px; font-weight: 550; }
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* форма */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
.note code { background: var(--panel2); padding: 1px 4px; border-radius: 4px; font-size: 10.5px; }
/* адрес + кнопка проверки в одной строке */
.addr-row { display: flex; gap: 8px; }
.addr-row input { flex: 1; }
.colors { display: flex; gap: 9px; }
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
.swatch.on { border-color: var(--fg); }
/* цвет ИЛИ картинка */
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
.appearance-tabs .tab {
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
}
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.pane { display: none; }
.pane.on { display: block; }
.pic-row { display: flex; align-items: center; gap: 13px; }
.pic-preview {
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
font-size: 20px; overflow: hidden;
}
.pic-preview.filled { border-style: solid; border-color: var(--line); }
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.avatar.pic { background: transparent; padding: 0; overflow: hidden; }
.avatar.pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* кнопки */
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.prim:disabled { background: var(--panel2); border-color: var(--line); color: var(--dim); cursor: default; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ============ ОКНО ПРОВЕРКИ СВЯЗИ ============ */
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
@keyframes sp { to { transform: rotate(360deg); } }
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 19px; flex: 0 0 40px; }
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
.probe-head.busy .big { background: transparent; padding: 0; }
.probe-head .txt { flex: 1; }
.probe-head .ttl { font-size: 14px; font-weight: 600; }
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px; }
.probe-list { font-size: 12px; }
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
border-bottom: 1px solid var(--line); }
.probe-list div:last-child { border-bottom: none; }
.probe-list span { color: var(--dim); }
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; }
.hint-line { font-size: 11.5px; color: var(--dim); margin-top: 12px; padding: 9px 11px;
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); }
/* переключатель состояний — только для показа */
.switcher {
position: fixed; top: 14px; left: 50%; transform: translateX(-50%); display: flex; gap: 7px;
background: rgba(23,33,43,.96); border: 1px solid var(--line); border-radius: 11px; padding: 7px 9px; z-index: 300;
}
.switcher button { background: transparent; border: none; color: var(--dim); font-size: 12px;
padding: 5px 11px; border-radius: 7px; cursor: pointer; font-family: inherit; }
.switcher button.on { background: var(--accent); color: #0F1922; font-weight: 600; }
/* ============ список диалогов с несколькими агентами ============ */
.group-head { display: flex; align-items: center; gap: 8px; padding: 11px 10px 6px; }
.group-head .dot { width: 9px; height: 9px; border-radius: 50%; }
.group-head .nm { font-size: 10.5px; letter-spacing: 1.1px; text-transform: uppercase; color: var(--dim); font-weight: 700; }
.group-head .cnt { font-size: 10.5px; color: var(--dim); margin-left: auto; }
.accent-bar { width: 3px; border-radius: 2px; flex: 0 0 3px; align-self: stretch; }
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.8); } }
@media (max-height: 700px) { .window { transform: scale(.68); } }
</style>
</head>
<body>
<div class="switcher">
<button data-view="list" class="on">Список агентов</button>
<button data-view="add">Добавить агента</button>
<button data-view="check" data-mode="ok">Проверка: отвечает</button>
<button data-view="check" data-mode="err">Проверка: не отвечает</button>
<button data-view="multi">Список диалогов</button>
</div>
<div class="window">
<!-- ===== фон: клиент с папками ===== -->
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="ic-btn" style="margin:0" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…">
<div class="folders">
<div class="folder active">Все чаты <span class="cnt">3</span></div>
<div class="folder">Работа</div>
<div class="folder">Дом</div>
<div class="folder">+</div>
</div>
<div class="chat-list" id="chatArea"></div>
</aside>
<main class="main">
<header class="chat-head">
<div class="avatar">АГ</div>
<div>
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
<button class="ic-btn" title="Настройки">⚙</button>
</header>
<div class="messages">
<div class="msg user">Проверь, почему падает CI</div>
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
</div>
</main>
<!-- ===== затемнение под окном настроек ===== -->
<div class="scrim on" id="scrim1"></div>
<!-- ===== ОКНО 1: настройки ===== -->
<div class="layer on" id="layerSettings">
<div class="dialog">
<div class="d-head">
<h2 id="dTitle">Агенты</h2>
<button class="x-btn" onclick="closeAll()">✕</button>
</div>
<!-- ── вид 1: список агентов ── -->
<div class="d-body" id="viewList">
<div class="agent-card">
<div class="avatar">АГ</div>
<div class="ag-mid">
<div class="ag-name">agentik · рабочий</div>
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
</div>
<div class="state"><span class="d"></span>на связи</div>
<div class="card-acts">
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="agent-card">
<div class="avatar g2">ДМ</div>
<div class="ag-mid">
<div class="ag-name">домашний</div>
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
</div>
<div class="state err"><span class="d"></span>не отвечает</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<button class="btn wide" onclick="show('add')">+ Добавить агента</button>
</div>
<!-- ── вид 2: форма ── -->
<div class="d-body" id="viewAdd" style="display:none">
<div class="field">
<label>Название</label>
<input type="text" value="рабочий" placeholder="как вы его будете звать">
<div class="note">Как называть этого агента в списке. Придумываете сами — сервер своего имени не сообщает.</div>
</div>
<div class="field">
<label>Адрес сервера агента</label>
<div class="addr-row">
<input type="text" class="mono" value="http://192.168.88.135:8080/agentik"
placeholder="http://хост:порт/agentik">
<button class="btn sm" onclick="openCheck('form')">Проверить связь</button>
</div>
<div class="note">Адрес целиком, вместе с путём <code>/agentik</code>. Если агент
на этой же машине — <code>http://localhost:8080/agentik</code>.</div>
</div>
<div class="field">
<label>Как отличать в списке</label>
<div class="appearance-tabs">
<div class="tab on" data-tab="color">Цвет</div>
<div class="tab" data-tab="pic">Картинка</div>
</div>
<!-- цвет -->
<div class="pane on" id="paneColor">
<div class="colors">
<div class="swatch on" style="background:#6AB2F2"></div>
<div class="swatch" style="background:#86C99A"></div>
<div class="swatch" style="background:#C9A886"></div>
<div class="swatch" style="background:#B79AE0"></div>
<div class="swatch" style="background:#E28C8C"></div>
</div>
<div class="note">Цвет остаётся простым выбором: им помечаются диалоги агента
в списке — полоска у строки и точка у группы.</div>
</div>
<!-- картинка -->
<div class="pane" id="panePic">
<div class="pic-row">
<div class="pic-preview" id="picPreview">🖼</div>
<div class="pic-actions">
<button class="btn sm" onclick="pickPic()">Выбрать файл…</button>
<button class="btn sm" id="picClear" onclick="clearPic()" style="display:none">Убрать</button>
</div>
</div>
<div class="note">Картинка копируется <b>внутрь клиента</b> — по исходному
адресу не подтягивается. Иначе значок пропадёт, если файл удалят,
флешку вынут или папку переименуют.</div>
</div>
</div>
</div>
<div class="d-foot" id="footList">
<button class="btn prim" style="flex:1" onclick="closeAll()">Готово</button>
</div>
<div class="d-foot" id="footAdd" style="display:none">
<button class="btn" style="flex:1" onclick="show('list')">Отмена</button>
<button class="btn prim" style="flex:1">Добавить агента</button>
</div>
</div>
</div>
<!-- ===== затемнение под окном проверки ===== -->
<div class="scrim top" id="scrim2"></div>
<!-- ===== ОКНО 2: проверка связи (поверх настроек) ===== -->
<div class="layer" id="layerCheck">
<div class="dialog small">
<div class="d-head">
<h2>Проверка связи</h2>
<button class="x-btn" onclick="closeCheck()">✕</button>
</div>
<div class="d-body">
<div class="probe-head" id="probeHead">
<div class="big" id="probeIcon"></div>
<div class="txt">
<div class="ttl" id="probeTitle">Спрашиваю агента…</div>
<div class="sub" id="probeSub">http://192.168.88.135:8080/agentik</div>
</div>
</div>
<div class="probe-list" id="probeList" style="display:none">
<div><span>Ответ на запрос</span><b id="p1">—</b></div>
<div><span>Отвечает по адресу</span><b id="p2">—</b></div>
<div><span>Диалогов у агента</span><b id="p3">—</b></div>
<div><span>Отклик</span><b id="p4">—</b></div>
</div>
<div class="hint-line" id="probeHint" style="display:none"></div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1" id="probeAgain" onclick="runCheck()">Проверить снова</button>
<button class="btn prim" style="flex:1" id="probeOk" onclick="closeCheck()" disabled>Добавить агента</button>
</div>
</div>
</div>
</div>
<script>
// ---------------- данные списка диалогов ----------------
const CHATS_SINGLE = `
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="chat-row">
<div class="avatar g3">РЗ</div>
<div class="chat-mid">
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Нашёл 3 ошибки 502</div>
</div>
</div>`;
const CHATS_MULTI = `
<div class="group-head"><span class="dot" style="background:#6AB2F2"></span>
<span class="nm">рабочий</span><span class="cnt">2 диалога</span></div>
<div class="chat-row active">
<div class="accent-bar" style="background:#6AB2F2"></div>
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>CI и релизы</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
</div>
<div class="chat-row">
<div class="accent-bar" style="background:#6AB2F2"></div>
<div class="avatar">ОЧ</div>
<div class="chat-mid">
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>
<div class="group-head"><span class="dot" style="background:#86C99A"></span>
<span class="nm">домашний</span><span class="cnt">1 диалог</span></div>
<div class="chat-row">
<div class="accent-bar" style="background:#86C99A"></div>
<div class="avatar g2">КН</div>
<div class="chat-mid">
<div class="chat-name"><span>Книги и полки</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Разложил по авторам</div>
</div>
</div>`;
const chatArea = document.getElementById('chatArea');
chatArea.innerHTML = CHATS_SINGLE;
// ---------------- окна ----------------
function el(id){ return document.getElementById(id); }
function show(v) {
const isList = (v === 'list');
el('viewList').style.display = isList ? 'block' : 'none';
el('viewAdd').style.display = isList ? 'none' : 'block';
el('footList').style.display = isList ? 'flex' : 'none';
el('footAdd').style.display = isList ? 'none' : 'flex';
el('dTitle').textContent = isList ? 'Агенты' : 'Новый агент';
openSettings();
}
function openSettings(){
el('scrim1').classList.add('on');
el('layerSettings').classList.add('on');
}
// закрыть окно настроек (вместе с окном проверки, если оно открыто)
function closeAll(){
el('layerCheck').classList.remove('on');
el('scrim2').classList.remove('on');
el('layerSettings').classList.remove('on');
el('scrim1').classList.remove('on');
}
function closeCheck(){
el('layerCheck').classList.remove('on');
el('scrim2').classList.remove('on');
}
// ---------------- ОКНО ПРОВЕРКИ СВЯЗИ ----------------
// отдельное окно поверх настроек: из формы нового агента или из списка
function openCheck(from, addr) {
el('scrim2').classList.add('on');
el('layerCheck').classList.add('on');
el('probeOk').textContent = (from === 'form') ? 'Добавить агента' : 'Готово';
window.__checkAddr = addr || 'http://192.168.88.135:8080/agentik';
runCheck();
}
function runCheck() {
const head = el('probeHead');
const list = el('probeList');
const hint = el('probeHint');
const icon = el('probeIcon');
const addr = window.__checkAddr || 'http://192.168.88.135:8080/agentik';
head.className = 'probe-head busy';
icon.outerHTML = '<span class="spin" id="probeIcon"></span>';
el('probeTitle').textContent = 'Спрашиваю агента…';
el('probeSub').textContent = addr;
list.style.display = 'none';
hint.style.display = 'none';
el('probeOk').disabled = true;
setTimeout(() => {
const sp = document.getElementById('probeIcon');
sp.outerHTML = '<div class="big" id="probeIcon"></div>';
const fail = (window.__checkMode === 'err');
if (fail) {
el('probeHead').className = 'probe-head err';
el('probeIcon').textContent = '✕';
el('probeTitle').textContent = 'Агент не отвечает';
el('probeSub').textContent = addr;
el('p1').textContent = 'нет ответа'; el('p2').textContent = '—';
el('p3').textContent = '—'; el('p4').textContent = '5000 мс (истекло)';
list.style.display = 'block';
hint.style.display = 'block';
hint.textContent = 'Проверьте адрес и что агент запущен. Убедитесь, что порт открыт и путь /agentik указан целиком.';
el('probeOk').disabled = true;
} else {
el('probeHead').className = 'probe-head ok';
el('probeIcon').textContent = '✓';
el('probeTitle').textContent = 'Агент отвечает';
el('probeSub').textContent = addr;
el('p1').textContent = 'ok'; el('p2').textContent = addr;
el('p3').textContent = '4'; el('p4').textContent = '38 мс';
list.style.display = 'block';
hint.style.display = 'block';
hint.textContent = 'Адрес рабочий. Можно добавлять этого агента.';
el('probeOk').disabled = false;
}
}, 1400);
}
// ---------------- переключатель для показа ----------------
document.querySelectorAll('.switcher button').forEach(b => b.onclick = () => {
document.querySelectorAll('.switcher button').forEach(x => x.classList.remove('on'));
b.classList.add('on');
const v = b.dataset.view;
if (v === 'multi') {
chatArea.innerHTML = CHATS_MULTI;
closeAll();
return;
}
chatArea.innerHTML = CHATS_SINGLE;
if (v === 'list') { show('list'); closeCheck(); }
if (v === 'add') { show('add'); closeCheck(); }
if (v === 'check') {
window.__checkMode = b.dataset.mode || 'ok';
const okAddr = 'http://192.168.88.135:8080/agentik';
const badAddr = 'http://192.168.76.199:8080/agentik';
show(window.__checkMode === 'ok' ? 'add' : 'list');
openCheck('form', window.__checkMode === 'ok' ? okAddr : badAddr);
}
});
document.querySelectorAll('.swatch').forEach(s => s.onclick = () => {
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('on'));
s.classList.add('on');
});
// ---------------- как отличать: цвет ИЛИ картинка ----------------
document.querySelectorAll('.appearance-tabs .tab').forEach(tab => tab.onclick = () => {
document.querySelectorAll('.appearance-tabs .tab').forEach(x => x.classList.remove('on'));
tab.classList.add('on');
const pic = (tab.dataset.tab === 'pic');
el('paneColor').classList.toggle('on', !pic);
el('panePic').classList.toggle('on', pic);
});
// выбор картинки: в настоящем клиенте — системный диалог выбора файла,
// а выбранный файл КОПИРУЕТСЯ в папку клиента (не ссылка на исходный путь)
function pickPic() {
// для макета показываем готовый пример вместо файлового диалога
setPic('data:image/svg+xml;utf8,' + encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54">
<rect width="54" height="54" fill="#86C99A"/>
<circle cx="27" cy="21" r="8" fill="#121218"/>
<path d="M10 50 Q27 30 44 50 Z" fill="#121218"/>
</svg>`));
}
function setPic(src) {
const p = el('picPreview');
p.innerHTML = `<img src="${src}" alt="">`;
p.classList.add('filled');
el('picClear').style.display = '';
}
function clearPic() {
const p = el('picPreview');
p.innerHTML = '🖼';
p.classList.remove('filled');
el('picClear').style.display = 'none';
}
</script>
</body>
</html>
-72
View File
@@ -1,72 +0,0 @@
# Вариант 5 — Новый диалог: выбор ассистента
> **МАКЕТ ОДОБРЕН 2026-09-19.** Одобренная версия — `approved/005-new-chat-picker.html`.
> Этот файл — черновик со всеми состояниями.
## Суть
Окно, которое появляется при нажатии «+» (новый диалог), **когда ассистентов
больше одного**. Спрашиваем, в каком ассистенте создать диалог.
**Когда окно показывается и когда нет:**
| Сколько ассистентов | Что происходит по «+» |
|---|---|
| **0** | Кнопки «+» **нет вообще** — создавать диалог не в ком |
| **1** | Окно **не показывается**: диалог создаётся сразу в этом ассистенте |
| **2 и больше** | Окно с выбором — этот макет |
## Состояния (переключатель сверху)
1. **Выбор: 2 ассистента** — основной вид. Сверху подсказка «В каком ассистенте
создать диалог?», ниже список. Первый отмечен **по умолчанию** — чтобы создать
диалог, достаточно одного нажатия.
2. **Третий не отвечает** — выбран ассистент без связи. Окно **предупреждает**,
но выбрать даёт: ассистент мог просто перезапускаться. Из списка не выкидываем.
3. **Ассистент один** — окна нет, затемнения нет.
4. **Ассистентов нет** — кнопки «+» нет, пустой список объясняет, что делать,
и ведёт в настройки.
5. **Диалог создан** — новый диалог первым в списке, шапка чата показывает
выбранного ассистента (его цвет и имя).
## Что видно в списке выбора
- **Значок ассистента** — цвет или картинка, как настроено в `004-settings-agents`.
- **Имя** и **адрес сервера** — чтобы различить похожие имена.
- **Состояние связи**: «на связи» зелёным, «не отвечает» красным.
- **Кружок отметки** справа.
- У ассистента без связи предупреждение появляется **при выборе**, а не сразу:
пока не выбран — шума нет.
## Решения
- **Выбран первый по умолчанию.** Открыл окно → сразу «Создать диалог» работает.
Лишнее движение нужно только если нужен не первый.
- **Ассистента без связи выбирать можно.** Запрещать нечестно: связь может
пропасть на секунду, а диалог человек создаёт осознанно. Окно предупреждает
словами, но не мешает.
- **Кнопку «+» при нуле ассистентов не рисуем.** Нажимать нечего — диалог
создавать не в ком.
- **Окно маленькое (480 px) и без вкладок.** Одно действие — один выбор.
Окно настроек (`004`) — 620 px, оно про управление; здесь просто выбор.
## Что НЕ делаем
- Не даём создавать диалог «без ассистента» — по библиотеке `client` диалог
живёт **внутри** агента, отдельно его не создать.
- Не показываем окно ради одного варианта — это лишнее нажатие.
- Не прячем ассистентов без связи (см. решения выше).
## Открытые вопросы
- Что делать, если **все** ассистенты не отвечают: показывать окно со всеми
красными или сразу писать «никто не отвечает»? Сейчас — показываем список.
- Нужен ли в этом окне шорткат «создать и сразу написать», то есть открывать
диалог с фокусом в поле ввода. Пока просто создаём.
- Нужна ли в окне ссылка «добавить нового ассистента» — сейчас её нет, за
добавлением идём в настройки.
## Из чего собрано
Стиль и классы взяты из `004-settings-agents` — тема та же (`#121218`, `#17212B`,
`#6AB2F2`), окно и затемнение устроены так же. Ничего нового не изобреталось.
-436
View File
@@ -1,436 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Новый диалог — выбор ассистента</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 100vh; padding: 24px; gap: 16px;
}
/* ============ переключатель состояний ============ */
.switcher { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; max-width: 1180px; }
.switcher button {
padding: 7px 13px; border-radius: 9px; border: 1px solid var(--line); background: var(--panel);
color: var(--dim); font-size: 12.5px; cursor: pointer; font-family: inherit;
}
.switcher button:hover { color: var(--fg); }
.switcher button.on { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
/* ============ окно клиента ============ */
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
.search {
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.search::placeholder { color: var(--dim); }
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
.folder {
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
}
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; align-items: flex-start; cursor: pointer; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.avatar {
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
/* аватар-картинка: клиент показывает СВОЮ копию файла */
.avatar.pic {
background: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E") center/cover;
}
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.row-tail { flex: 0 0 auto; }
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700; border-radius: 10px; padding: 1px 7px; }
/* кнопка «+» рядом с заголовком. Состояние .hidden — при нуле ассистентов */
.ic-btn { width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 16px; }
.ic-btn:hover { background: var(--panel2); color: var(--fg); }
.add-btn { font-size: 19px; color: var(--accent); }
.add-btn.hidden { display: none; }
/* пустой список */
.empty { padding: 44px 26px; text-align: center; }
.empty .big { font-size: 34px; opacity: .28; margin-bottom: 12px; }
.empty .ttl { font-size: 14px; font-weight: 600; margin-bottom: 7px; }
.empty .sub { font-size: 12.5px; color: var(--dim); line-height: 1.55; }
.empty .link { color: var(--accent); cursor: pointer; text-decoration: none; border-bottom: 1px dashed var(--accent); }
.main { flex: 1; display: flex; flex-direction: column; }
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.chat-head .ic-btn { margin-left: auto; }
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
/* ============ затемнение и окно ============ */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; display: none; }
.scrim.on { display: block; }
.layer { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; z-index: 150; pointer-events: none; }
.layer.on { display: flex; }
.layer > * { pointer-events: auto; }
.dialog {
width: 480px; min-width: 480px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
/* ── вариант выбора ассистента ── */
.agent-opt {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; cursor: pointer;
border: 1.5px solid transparent;
}
.agent-opt:hover { border-color: #33414F; }
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
.agent-opt .avatar { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
.ag-mid { flex: 1; min-width: 0; }
.ag-name { font-size: 14px; font-weight: 550; }
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
.ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
.ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.ag-state.ok { color: var(--ok); }
.ag-state.off { color: var(--dim); } .ag-state.off .d { background: var(--dim); }
.ag-state.err { color: var(--err); } .ag-state.err .d { background: var(--err); }
/* кружок-галочка выбора */
.pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%; border: 2px solid #3D4A58; }
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
.agent-opt.on .pick::after {
content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 9px;
border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg);
}
/* предупреждение под списком */
.warn {
display: none; gap: 8px; align-items: flex-start; margin-top: 4px; padding: 10px 12px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA;
}
.warn.on { display: flex; }
.warn .i { flex: 0 0 auto; }
/* кнопки */
.btn {
padding: 10px 16px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel2);
color: var(--fg); font-size: 13.5px; cursor: pointer; font-family: inherit;
}
.btn:hover { border-color: #3D4A58; }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
.btn.prim:disabled { background: #253544; border-color: #253544; color: #5C6B7A; cursor: not-allowed; }
</style>
</head>
<body>
<div class="switcher">
<button data-view="pick" class="on">Выбор: 2 ассистента</button>
<button data-view="offline">Третий не отвечает</button>
<button data-view="one">Ассистент один</button>
<button data-view="zero">Ассистентов нет</button>
<button data-view="made">Диалог создан</button>
</div>
<div class="window">
<!-- ================= СПИСОК ДИАЛОГОВ ================= -->
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="ic-btn add-btn" id="newChatBtn" title="Новый диалог">+</button>
</div>
</div>
<input class="search" placeholder="Поиск диалога…" id="searchBox">
<div class="folders" id="folders">
<div class="folder active">Все чаты <span class="cnt" id="cnt">2</span></div>
<div class="folder">Работа</div>
<div class="folder">Дом</div>
<div class="folder">+</div>
</div>
<div class="chat-list" id="chatList"></div>
</aside>
<!-- ================= ЧАТ ================= -->
<main class="main" id="mainPane">
<header class="chat-head">
<div class="avatar" id="headAv">АГ</div>
<div>
<div class="head-name" id="headName">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
<button class="ic-btn" title="Настройки">⚙</button>
</header>
<div class="messages" id="msgs">
<div class="msg user">Проверь, почему падает CI</div>
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
</div>
</main>
<div class="scrim" id="scrim"></div>
<!-- ================= ОКНО: НОВЫЙ ДИАЛОГ ================= -->
<div class="layer" id="layerNew">
<div class="dialog">
<div class="d-head">
<h2>Новый диалог</h2>
<button class="x-btn" onclick="closeDialog()">✕</button>
</div>
<div class="d-body">
<div class="d-lead" id="lead">В каком ассистенте создать диалог?</div>
<div id="agentOpts">
<div class="agent-opt on" data-id="a1" data-ok="1" data-tag="АГ">
<div class="avatar">АГ</div>
<div class="ag-mid">
<div class="ag-name">agentik · рабочий</div>
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
<div class="ag-state ok"><span class="d"></span>на связи</div>
</div>
<div class="pick"></div>
</div>
<div class="agent-opt" data-id="a2" data-ok="1" data-tag="ДМ">
<div class="avatar g2">ДМ</div>
<div class="ag-mid">
<div class="ag-name">домашний</div>
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
<div class="ag-state ok"><span class="d"></span>на связи</div>
</div>
<div class="pick"></div>
</div>
<div class="agent-opt" data-id="a3" data-ok="0" data-tag="КА" data-pic="1">
<div class="avatar pic"></div>
<div class="ag-mid">
<div class="ag-name">Кот-ассистент</div>
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
<div class="ag-state err"><span class="d"></span>не отвечает</div>
</div>
<div class="pick"></div>
</div>
</div>
<div class="warn" id="warn">
<span class="i">⚠</span>
<span id="warnText">Ассистент сейчас не отвечает. Диалог создать можно, но
сообщение не уйдёт, пока он не поднимется.</span>
</div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1" onclick="closeDialog()">Отмена</button>
<button class="btn prim" style="flex:1" id="createBtn" disabled>Создать диалог</button>
</div>
</div>
</div>
</div>
<script>
const el = id => document.getElementById(id);
// ───────── данные состояний ─────────
const AG_ALL = `
<div class="agent-opt on" data-id="a1" data-ok="1">
<div class="avatar">АГ</div>
<div class="ag-mid">
<div class="ag-name">agentik · рабочий</div>
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
<div class="ag-state ok"><span class="d"></span>на связи</div>
</div>
<div class="pick"></div>
</div>
<div class="agent-opt" data-id="a2" data-ok="1">
<div class="avatar g2">ДМ</div>
<div class="ag-mid">
<div class="ag-name">домашний</div>
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
<div class="ag-state ok"><span class="d"></span>на связи</div>
</div>
<div class="pick"></div>
</div>
<div class="agent-opt" data-id="a3" data-ok="0">
<div class="avatar pic"></div>
<div class="ag-mid">
<div class="ag-name">Кот-ассистент</div>
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
<div class="ag-state err"><span class="d"></span>не отвечает</div>
</div>
<div class="pick"></div>
</div>`;
const CHATS_TWO = `
<div class="chat-row active">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>
<div class="chat-row">
<div class="avatar g2">ДМ</div>
<div class="chat-mid">
<div class="chat-name"><span>домашний</span><span class="chat-time">12:05</span></div>
<div class="chat-prev">Файл лежит на диске</div>
</div>
</div>`;
const CHATS_NEW = `
<div class="chat-row active">
<div class="avatar g2">ДМ</div>
<div class="chat-mid">
<div class="chat-name"><span>домашний</span><span class="chat-time">сейчас</span></div>
<div class="chat-prev">Новый диалог</div>
</div>
</div>
<div class="chat-row">
<div class="avatar">АГ</div>
<div class="chat-mid">
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
<div class="chat-prev">Собрал отчёт, жду правок</div>
</div>
<div class="row-tail"><span class="badge">2</span></div>
</div>`;
const CHATS_EMPTY = `
<div class="empty">
<div class="big">💬</div>
<div class="ttl">Диалогов пока нет</div>
<div class="sub">Ассистенты ещё не добавлены.<br>
Добавьте первого в <a class="link" onclick="alert('Откроется окно настроек — макет 004-settings-agents')">настройках</a>.</div>
</div>`;
// ───────── управление окном ─────────
function setAgents(html) { el('agentOpts').innerHTML = html; bindOpts(); }
function bindOpts() {
document.querySelectorAll('.agent-opt').forEach(o => o.onclick = () => {
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
o.classList.add('on');
el('createBtn').disabled = false;
el('warn').classList.toggle('on', o.dataset.ok === '0');
});
}
function openDialog() {
el('scrim').classList.add('on');
el('layerNew').classList.add('on');
el('createBtn').disabled = false;
}
function closeDialog() {
el('scrim').classList.remove('on');
el('layerNew').classList.remove('on');
}
el('newChatBtn').onclick = openDialog;
document.querySelectorAll('.switcher button').forEach(b => b.onclick = () => {
document.querySelectorAll('.switcher button').forEach(x => x.classList.remove('on'));
b.classList.add('on');
render(b.dataset.view);
});
function render(v) {
closeDialog();
el('warn').classList.remove('on');
if (v === 'pick') {
el('chatList').innerHTML = CHATS_TWO;
el('folders').style.display = ''; el('searchBox').style.display = '';
el('cnt').textContent = '2';
el('newChatBtn').classList.remove('hidden');
setAgents(AG_ALL);
el('createBtn').disabled = false;
openDialog();
}
if (v === 'offline') {
el('chatList').innerHTML = CHATS_TWO;
el('folders').style.display = ''; el('searchBox').style.display = '';
el('cnt').textContent = '2';
el('newChatBtn').classList.remove('hidden');
setAgents(AG_ALL);
const off = document.querySelector('.agent-opt[data-ok="0"]');
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
off.classList.add('on');
el('warn').classList.add('on');
el('createBtn').disabled = false;
openDialog();
}
if (v === 'one') {
el('chatList').innerHTML = CHATS_TWO;
el('folders').style.display = ''; el('searchBox').style.display = '';
el('cnt').textContent = '2';
el('newChatBtn').classList.remove('hidden');
}
if (v === 'zero') {
el('chatList').innerHTML = CHATS_EMPTY;
el('folders').style.display = 'none'; el('searchBox').style.display = 'none';
el('newChatBtn').classList.add('hidden');
}
if (v === 'made') {
el('chatList').innerHTML = CHATS_NEW;
el('folders').style.display = ''; el('searchBox').style.display = '';
el('cnt').textContent = '2';
el('newChatBtn').classList.remove('hidden');
el('headAv').textContent = 'ДМ'; el('headAv').className = 'avatar g2';
el('headName').textContent = 'домашний';
}
}
bindOpts();
render('pick');
</script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
# sketches — макеты основного окна
**Здесь рабочий черновик основного окна десктопного клиента.** Это не
«утверждено» — `approved/` живёт отдельно. Правило: одобренное живёт только
там, а в `sketches/` — то, что ещё сравниваем и допиливаем.
## Раскладка: rail / voice-first
Узкая рейка иконок слева (диалоги / память / инструменты / новый),
посередине список диалогов, справа чат. Голосовое сообщение — главное
действие: крупная акцентная кнопка микрофона в композере.
**Цветовая схема:** тёмная тема, общий набор токенов из `../THEMES.md`.
Палитра согласована с Android-клиентом — расширения (`--panel2`, `--line`,
`--badge`, `--ok`, `--user-bubble`, `--bot-bubble`, `--a1`..`--a5`) тут
уже есть.
## Файлы
| Файл | Состояние |
|---|---|
| `style.css` | Общий CSS для всех макетов |
| `1-chats.html` | Слева — список диалогов с цветными полосками агентов, справа — пусто («выберите диалог») |
| `2-chat.html` | Открытый диалог: сообщения, ход работы агента, композер с полем + микрофон + отправка |
| `3-recording.html` | Идёт запись — в композере волна + таймер + ⏸ + ✕ + ➤, одна строка 46 px |
| `4-empty.html` | Список пуст, ни одного ассистента не добавлено |
| `5-new-chat.html` | Модалка по центру — выбор ассистента из добавленных |
| `6-groups.html` | Модалка управления группами: список с действиями «изменить» / «удалить» |
| `7-groups-add.html` | Создание группы: имя + пустой список диалогов + кнопка «+ Добавить диалог» |
| `8-groups-edit.html` | Переименование + теги уже добавленных диалогов с `✕` + блок «ОПАСНОЕ» с удалением группы |
| `9-groups-pick-chat.html` | Выбор диалога для добавления в группу: поиск + чекбоксы по диалогам |
## Как смотреть
Открыть файл в браузере — самодостаточный, без сборки и без зависимостей.
+1426
View File
File diff suppressed because it is too large Load Diff
+610
View File
@@ -0,0 +1,610 @@
/* === палитра тёмной темы (согласована с Android, см. ../THEMES.md) === */
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A;
--user-bubble: #2B5278; --bot-bubble: #1C2733;
--a1: #6AB2F2; --a2: #86C99A; --a3: #C9A886; --a4: #B79AE0; --a5: #E28C8C;
--on-accent: #0F1922;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 18px;
}
.win {
width: 1240px; height: 760px; background: var(--bg);
border-radius: 14px; overflow: hidden; border: 1px solid #2A3340;
display: flex; box-shadow: 0 28px 90px rgba(0,0,0,.7);
}
.cap {
position: absolute; margin-top: -22px; font-size: 12px; color: var(--dim);
}
@media (max-height: 820px) { body { padding: 8px; } .win { transform: scale(.78); } }
@media (max-height: 700px) { .win { transform: scale(.66); } }
@media (max-height: 600px) { .win { transform: scale(.56); } }
/* === средняя колонка: список диалогов === */
.list {
width: 296px; flex: 0 0 296px; background: var(--panel); border-right: 1px solid var(--line);
display: flex; flex-direction: column;
}
.l-head { padding: 14px 16px 10px; display: flex; align-items: center; gap: 8px; }
.l-head h1 { font-size: 15.5px; font-weight: 650; letter-spacing: .2px; }
.chips { display: flex; gap: 6px; padding: 0 14px 10px; overflow-x: auto; }
.chip {
padding: 6px 11px; border-radius: 16px; font-size: 12px; color: var(--dim);
background: transparent; white-space: nowrap; border: 1px solid transparent;
}
.chip:hover { background: var(--panel2); }
.chip.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.13); }
.chip b { color: var(--badge); font-weight: 700; margin-left: 3px; }
.chip-act {
margin-left: auto; flex: 0 0 auto; padding: 6px 9px; border-radius: 11px;
background: transparent; color: var(--dim); font-size: 13px; cursor: pointer;
border: 1px solid transparent; display: inline-flex; align-items: center;
justify-content: center;
}
.chip-act:hover { color: var(--fg); background: var(--panel2); }
.chip-act svg { width: 14px; height: 14px; stroke: currentColor; fill: none;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.q {
margin: 0 14px 10px; height: 36px; border-radius: 11px; border: 1px solid var(--line);
background: var(--bg); color: var(--dim); font-size: 12.5px;
display: flex; align-items: center; gap: 8px; padding: 0 12px;
}
.rows { flex: 1; overflow-y: auto; padding: 0 8px 12px; }
.row {
display: flex; gap: 11px; padding: 9px 10px; border-radius: 12px; cursor: pointer;
position: relative;
}
.row:hover { background: var(--panel2); }
.row.on { background: #233040; }
.row .strip {
position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px;
background: var(--a1); border-radius: 3px;
}
.row .ava {
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%;
display: grid; place-items: center; font-size: 14px; font-weight: 700; color: var(--on-accent);
}
.row .mid { flex: 1; min-width: 0; }
.row .l1 { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.row .l1 .n { font-size: 13.5px; font-weight: 550; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .l1 .t { font-size: 10.5px; color: var(--dim); flex: 0 0 auto; }
.row .l2 { display: flex; justify-content: space-between; gap: 8px; align-items: center; margin-top: 2px; }
.row .l2 .p { font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bd { background: var(--badge); color: var(--on-accent); border-radius: 11px; font-size: 10.5px; font-weight: 700; padding: 1px 6px; flex: 0 0 auto; }
/* === основная зона чата === */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.m-head {
height: 60px; flex: 0 0 60px; display: flex; align-items: center; gap: 11px;
padding: 0 22px; border-bottom: 1px solid var(--line);
}
.m-head .ava { width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; font-size: 14px; font-weight: 700; color: var(--on-accent); }
.m-head .hd .t1 { font-size: 14.5px; font-weight: 600; }
.m-head .hd .t2 { font-size: 11.5px; color: var(--dim); }
.m-head .hd .t2.ok { color: var(--ok); }
.m-head .hd .t2.bad { color: var(--err); }
.m-head .ghost { margin-left: auto; display: flex; gap: 6px; }
.m-head .ghost button {
background: transparent; border: 1px solid var(--line); color: var(--dim);
border-radius: 8px; padding: 5px 11px; font-size: 11.5px; cursor: pointer;
}
.m-head .ghost button:hover { color: var(--fg); border-color: var(--dim); }
.thread { flex: 1; overflow-y: auto; padding: 20px 26px; display: flex; flex-direction: column; gap: 11px; }
.b {
max-width: 76%; padding: 9px 13px; border-radius: 14px; font-size: 13.5px;
}
.b.u { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 5px; }
.b.a { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 5px; }
.b b { font-weight: 650; }
.b code { background: rgba(255,255,255,.08); border-radius: 4px; padding: 1px 5px; font-size: 12px; font-family: "DejaVu Sans Mono", monospace; }
.b .tm { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
.cur { display: inline-block; width: 7px; height: 13px; background: var(--accent); vertical-align: -2px; margin-left: 2px; }
/* ход работы агента — компактная раскрывающаяся строка */
.act-line {
align-self: flex-start; display: flex; align-items: center; gap: 9px;
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
padding: 7px 12px; font-size: 12.5px; color: var(--dim); max-width: 78%;
}
.act-line .g { color: var(--accent); }
.act-line .nm { color: var(--fg); }
.act-line .ch { margin-left: auto; font-size: 11px; }
/* === композер: voice-first — микрофон главный === */
.composer { border-top: 1px solid var(--line); background: var(--panel); padding: 14px 22px 14px; }
.rec-panel {
display: flex; align-items: center; gap: 10px; background: var(--bg);
border: 1px solid var(--err); border-radius: 13px; padding: 4px 6px 4px 13px; height: 46px;
}
.wave { display: flex; align-items: center; gap: 3px; height: 28px; flex: 1; min-width: 0; }
.wave i { width: 3px; background: var(--err); border-radius: 2px; }
.wave i:nth-child(1) { height: 30%; } .wave i:nth-child(2) { height: 70%; }
.wave i:nth-child(3) { height: 45%; } .wave i:nth-child(4) { height: 100%; }
.wave i:nth-child(5) { height: 55%; } .wave i:nth-child(6) { height: 80%; }
.wave i:nth-child(7) { height: 35%; } .wave i:nth-child(8) { height: 65%; }
.wave i:nth-child(9) { height: 25%; }
.rec-time {
font-variant-numeric: tabular-nums; font-size: 13px; color: var(--err);
font-weight: 700; flex: 0 0 auto;
}
.rec-btn {
width: 34px; height: 34px; flex: 0 0 34px; border-radius: 10px;
background: transparent; border: none; color: var(--dim); font-size: 14px;
display: grid; place-items: center; cursor: pointer;
}
.rec-btn:hover { color: var(--fg); background: var(--panel2); }
.rec-btn.rec-send { background: var(--accent); color: var(--on-accent); font-size: 15px; }
.rec-btn.rec-send:hover { background: #82BEF5; }
.composer-row { display: flex; align-items: center; gap: 10px; }
.field {
flex: 1; min-width: 0; height: 46px; border-radius: 13px; background: var(--bg);
border: 1px solid var(--line); display: flex; align-items: center; padding: 0 14px;
font-size: 13.5px; color: var(--dim);
}
.field.focus { border-color: var(--accent); }
.mic {
width: 52px; height: 52px; flex: 0 0 52px; border-radius: 50%;
border: 2px solid var(--accent); background: var(--bg); color: var(--accent);
font-size: 19px; display: grid; place-items: center; cursor: pointer;
}
.mic:hover { background: rgba(106,178,242,.13); }
.mic.rec { background: var(--err); border-color: var(--err); color: #fff; }
.send {
width: 46px; height: 46px; flex: 0 0 46px; border-radius: 13px; border: none;
background: var(--accent); color: var(--on-accent); font-size: 17px;
display: grid; place-items: center; cursor: pointer;
}
.send.off { background: var(--panel2); color: var(--dim); cursor: default; }
.rec-note { font-size: 11px; color: var(--dim); margin-top: 7px; text-align: center; }
/* стрим: вместо поля — широкая красная кнопка «Стоп» */
.stop-wide {
height: 50px; border-radius: 13px; border: 1px solid rgba(236,57,66,.45);
background: rgba(236,57,66,.10); color: #F0A6AA; font-size: 13.5px; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 9px; cursor: pointer;
}
/* === пустое состояние === */
.empty {
flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; text-align: center; padding: 30px 34px; gap: 14px;
}
.empty .big {
width: 78px; height: 78px; border-radius: 22px; background: var(--panel);
border: 1px solid var(--line); display: grid; place-items: center; font-size: 32px;
color: var(--dim);
}
.empty h3 { font-size: 17px; font-weight: 600; }
.empty p { font-size: 13px; color: var(--dim); line-height: 1.55; max-width: 340px; }
.btn {
border-radius: 12px; padding: 11px 18px; font-size: 13.5px; cursor: pointer;
border: 1px solid var(--line); background: transparent; color: var(--fg); font-family: inherit;
}
.btn.prim { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 650; }
/* === модалка нового диалога === */
.scrim {
position: absolute; inset: 0; background: rgba(0,0,0,.6);
display: flex; align-items: center; justify-content: center; padding: 22px;
}
.modal {
width: 480px; max-height: 84%; background: var(--panel);
border: 1px solid var(--line); border-radius: 16px; display: flex;
flex-direction: column; overflow: hidden;
}
.modal h2 { font-size: 16px; font-weight: 650; padding: 16px 20px 4px; }
.modal .sub { font-size: 12.5px; color: var(--dim); padding: 0 20px 12px; }
.modal .body { flex: 1; overflow-y: auto; padding: 4px 12px 8px; }
.agent {
display: flex; align-items: center; gap: 12px; padding: 13px 14px;
border-radius: 12px; cursor: pointer; border: 1px solid transparent;
}
.agent:hover { background: var(--panel2); }
.agent.on { border-color: var(--accent); background: rgba(106,178,242,.07); }
.agent .sign { width: 38px; height: 38px; flex: 0 0 38px; border-radius: 50%; display: grid; place-items: center; font-size: 14px; font-weight: 700; color: var(--on-accent); }
.agent .mid { flex: 1; min-width: 0; }
.agent .nm { font-size: 14px; font-weight: 550; }
.agent .ad { font-size: 11.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent .ok { color: var(--ok); font-size: 11.5px; }
.agent .bad { color: var(--err); font-size: 11.5px; }
.agent .radio { width: 22px; height: 22px; flex: 0 0 22px; border-radius: 50%; border: 2px solid var(--line); }
.agent.on .radio { border-color: var(--accent); background: radial-gradient(circle, var(--accent) 0 6px, transparent 7px); }
.modal .foot { padding: 12px 16px 16px; display: flex; gap: 10px; border-top: 1px solid var(--line); }
.modal .foot .btn:first-child { flex: 1; }
.modal .warn {
margin: 0 14px 10px; padding: 10px 12px; border-radius: 11px;
background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA;
}
/* === модалка управления группами === */
.modal.groups { width: 540px; }
.modal.groups h2 { display: flex; align-items: center; gap: 10px; padding: 14px 20px 6px; }
.modal.groups h2 .x { margin-left: auto; width: 28px; height: 28px; border-radius: 8px; border: none; background: transparent; color: var(--dim); cursor: pointer; font-size: 16px; }
.modal.groups h2 .x:hover { background: var(--panel2); color: var(--fg); }
.modal.groups .body { padding: 6px 14px 10px; }
.grp {
display: flex; align-items: center; gap: 12px; padding: 11px 13px;
border-radius: 11px; background: var(--panel2); margin-bottom: 8px;
border: 1px solid transparent;
}
.grp:hover { border-color: var(--line); }
.grp .mid { flex: 1; min-width: 0; }
.grp .nm { font-size: 14px; font-weight: 550; }
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 2px; }
.grp .acts { display: flex; gap: 4px; }
.grp.sys { background: transparent; border: 1px dashed var(--line); }
.grp .lock { font-size: 11px; color: var(--dim); }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; display: inline-flex;
align-items: center; justify-content: center; line-height: 1; }
.mini-ic svg, .card-acts svg { width: 16px; height: 16px; stroke: currentColor; fill: none;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.mini-ic.del svg, .card-acts.del svg { fill: currentColor; stroke: none; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
.modal .info {
display: flex; gap: 9px; align-items: flex-start; margin: 4px 14px 12px;
padding: 10px 13px; border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
font-size: 11.5px; color: var(--dim); line-height: 1.5;
}
.modal .danger-zone { margin: 14px 14px 0; padding-top: 14px; border-top: 1px solid var(--line); }
.modal .danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 8px; }
.modal .form-field { margin: 0 14px 14px; }
.modal .form-field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.modal .form-field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.modal .form-field input:focus { border-color: var(--accent); }
.modal .form-field .note { font-size: 11px; color: var(--dim); margin-top: 6px; line-height: 1.5; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
/* === теги диалогов в группе + кнопка «добавить диалог» === */
.modal .tags { margin: 0 14px 12px; }
.modal .tags .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
.modal .tags .empty { font-size: 12px; color: var(--dim); padding: 9px 12px;
background: var(--bg); border: 1px dashed var(--line); border-radius: 10px; }
.modal .tags-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 9px; }
.chat-tag {
display: inline-flex; align-items: center; gap: 7px;
padding: 4px 8px 4px 4px; background: var(--bg);
border: 1px solid var(--line); border-radius: 9px;
}
.chat-tag .ava-sm { width: 22px; height: 22px; flex: 0 0 22px; border-radius: 50%;
display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--on-accent); }
.chat-tag .nm { font-size: 12.5px; }
.chat-tag .x { width: 18px; height: 18px; border-radius: 50%; border: none;
background: transparent; color: var(--dim); cursor: pointer;
font-size: 10px; display: grid; place-items: center; }
.chat-tag .x:hover { color: var(--err); background: rgba(236,57,66,.12); }
.add-chat-btn {
display: flex; align-items: center; justify-content: center; gap: 7px;
padding: 9px; width: 100%; cursor: pointer; font-size: 13px; font-family: inherit;
background: transparent; color: var(--dim);
border: 1px dashed var(--line); border-radius: 10px;
}
.add-chat-btn:hover { color: var(--fg); border-color: var(--dim); }
/* === модалка выбора диалога для группы === */
.modal.pick { width: 460px; }
.modal.pick .q {
margin: 4px 14px 8px; padding: 9px 12px; border-radius: 10px;
border: 1px solid var(--line); background: var(--bg);
color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
}
.modal.pick .q:focus { border-color: var(--accent); }
.modal.pick .q::placeholder { color: var(--dim); }
.modal.pick .body { padding: 0 8px 8px; max-height: 360px; }
.pick-row {
display: flex; align-items: center; gap: 11px; padding: 9px 10px;
border-radius: 10px; cursor: pointer; border: 1px solid transparent;
}
.pick-row:hover { background: var(--panel2); }
.pick-row.in { opacity: .42; cursor: default; }
.pick-row .ava-sm { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 50%;
display: grid; place-items: center; font-size: 12px; font-weight: 700; color: var(--on-accent); }
.pick-row .mid { flex: 1; min-width: 0; }
.pick-row .nm { font-size: 13.5px; }
.pick-row .ad { font-size: 11.5px; color: var(--dim); margin-top: 1px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pick-row .check { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 6px;
border: 1.5px solid var(--line); display: grid; place-items: center; color: transparent; font-size: 11px; }
.pick-row .check.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
/* === 004 · окно агентов (список + форма + поповер проверки) === */
.dialog {
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px;
background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
display: flex; flex-direction: column; overflow: hidden;
box-shadow: 0 26px 80px rgba(0,0,0,.7);
}
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px;
border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
.agent-card {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.agent-card:hover { border-color: var(--line); }
.ag-mid { flex: 1; min-width: 0; }
.ag-name { font-size: 14px; font-weight: 550; }
.ag-desc { font-size: 11.5px; color: var(--fg); opacity: .7;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; line-height: 1.5; }
.note code, .hint-line code { background: var(--panel2); padding: 1px 4px; border-radius: 4px;
font-size: 10.5px; }
.addr-row { display: flex; gap: 8px; }
.addr-row input { flex: 1; }
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
.appearance-tabs .tab {
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
}
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent);
background: rgba(106,178,242,.12); }
.pane { display: none; } .pane.on { display: block; }
.colors { display: flex; gap: 9px; }
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
border: 2px solid transparent; }
.swatch.on { border-color: var(--fg); }
.pic-row { display: flex; align-items: center; gap: 13px; }
.pic-preview {
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
font-size: 20px; overflow: hidden;
}
.pic-preview.filled { border-style: solid; }
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
.hint-line { font-size: 11.5px; color: var(--dim); margin: 2px 0 16px; padding: 9px 11px;
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); line-height: 1.5; }
/* поповер проверки связи */
.layer { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
z-index: 250; }
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
@keyframes sp { to { transform: rotate(360deg); } }
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 19px; flex: 0 0 40px; }
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
.probe-head.busy .big { background: transparent; padding: 0; }
.probe-head .txt { flex: 1; min-width: 0; }
.probe-head .ttl { font-size: 14px; font-weight: 600; }
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.probe-list { font-size: 12px; }
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
border-bottom: 1px solid var(--line); }
.probe-list div:last-child { border-bottom: none; }
.probe-list span { color: var(--dim); }
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11.5px; }
/* === окно клиента .window + сайдбар .sidebar (вариант sidebar-utility) === */
.window {
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
box-shadow: 0 24px 80px rgba(0,0,0,.6);
}
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line);
display: flex; flex-direction: column; flex: 0 0 320px; }
.sb-head { padding: 14px 16px 10px; }
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center;
justify-content: space-between; }
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none;
background: transparent; color: var(--dim); cursor: pointer; display: grid; place-items: center;
font-size: 17px; }
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
.search { padding: 8px 12px; border-radius: 9px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13px; outline: none; font-family: inherit; }
.search:focus { border-color: var(--accent); }
.search::placeholder { color: var(--dim); }
/* лента папок (sidebar-utility) */
.folders { display: flex; gap: 6px; padding: 6px 16px 10px; overflow-x: auto; }
.folder { padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent; }
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
.folder.act svg { width: 14px; height: 14px; stroke: currentColor; fill: none;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.folder.act:hover { color: var(--fg); background: var(--panel2); }
.chat-list { flex: 1; overflow-y: auto; padding: 4px 8px 12px; }
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer;
align-items: flex-start; }
.chat-row:hover { background: var(--panel2); }
.chat-row.active { background: #233040; }
.chat-mid { flex: 1; min-width: 0; }
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; flex: 0 0 auto; }
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin-top: 2px; }
.badge { background: var(--badge); color: #0F1922; border-radius: 10px; font-size: 11px;
font-weight: 700; padding: 1px 6px; min-width: 19px; text-align: center; }
.row-tail { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: 0 0 auto; }
/* правая часть .main — заголовок чата */
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line);
display: flex; align-items: center; gap: 11px; padding: 0 16px; }
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
.head-meta { flex: 1; }
.head-name { font-size: 14.5px; font-weight: 600; }
.head-sub { font-size: 11.5px; color: var(--dim); }
.head-sub .dot { color: var(--ok); }
.back-btn { display: none; width: 32px; height: 32px; flex: 0 0 32px; border-radius: 8px;
border: none; background: transparent; color: var(--fg); cursor: pointer; font-size: 17px;
place-items: center; }
.back-btn:hover { background: var(--panel); }
/* сообщения */
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column;
gap: 12px; }
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
.msg .meta { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
.think { align-self: flex-start; max-width: 80%; border-left: 2px solid var(--accent);
background: rgba(106,178,242,.07); padding: 8px 12px; border-radius: 0 10px 10px 0;
font-size: 12.5px; color: var(--dim); }
.think b { color: var(--accent); font-weight: 600; }
.think .rest { display: none; }
.tool { align-self: flex-start; max-width: 80%; background: var(--panel); border: 1px solid var(--line);
border-radius: 10px; padding: 8px 12px; font-size: 12.5px;
display: flex; align-items: center; gap: 9px; }
.tool .gear { color: var(--accent); }
.tool code { color: var(--dim); font-size: 11.5px; }
/* композер */
.composer { border-top: 1px solid var(--line); padding: 12px 16px 14px; background: var(--panel); }
.attach-strip { display: flex; gap: 8px; margin-bottom: 9px; }
.attach-strip:empty { display: none; }
.composer .chip { background: var(--panel2); border: 1px solid var(--line); border-radius: 8px;
padding: 5px 9px; font-size: 11.5px; color: var(--dim); display: flex; align-items: center; gap: 7px; }
.composer .chip .x { cursor: pointer; color: var(--err); font-weight: 700; }
.input-row { display: flex; align-items: flex-end; gap: 9px; }
.input-wrap { flex: 1; display: flex; align-items: flex-end; background: var(--bg);
border: 1px solid var(--line); border-radius: 11px; padding: 9px 12px; gap: 9px; }
.input-wrap:focus-within { border-color: var(--accent); }
.input-wrap textarea { flex: 1; background: transparent; border: none; outline: none; resize: none;
color: var(--fg); font-family: inherit; font-size: 13.5px; line-height: 1.45; max-height: 120px;
height: 22px; }
.input-wrap textarea::placeholder { color: var(--dim); }
.rec-btn { width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line);
background: var(--bg); color: var(--dim); cursor: pointer; display: grid; place-items: center;
font-size: 17px; flex: 0 0 40px; transition: all .18s; }
.rec-btn:hover { color: var(--fg); border-color: var(--dim); }
.rec-btn.recording { background: var(--err); border-color: var(--err); color: #fff;
animation: pulse 1.4s infinite; }
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(236,57,66,.55); }
50% { box-shadow: 0 0 0 9px rgba(236,57,66,0); } }
.send-btn { height: 40px; padding: 0 18px; border-radius: 11px; border: none;
background: var(--accent); color: #0F1922; font-size: 13px; font-weight: 600; cursor: pointer; }
.send-btn:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
.hint { font-size: 11px; color: var(--dim); margin-top: 8px; display: flex;
justify-content: space-between; }
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px;
padding: 1px 5px; font-size: 10.5px; }
/* запись — полоса над полем */
.rec-panel { display: flex; align-items: center; gap: 12px; margin-bottom: 10px;
background: var(--bg); border: 1px solid var(--err); border-radius: 11px; padding: 9px 13px; }
.wave { display: flex; align-items: center; gap: 3px; height: 22px; flex: 1; }
.wave i { width: 3px; background: var(--err); border-radius: 2px; height: 30%;
animation: wv .9s infinite ease-in-out; }
.wave i:nth-child(2n) { animation-delay: .12s; }
.wave i:nth-child(3n) { animation-delay: .24s; }
.wave i:nth-child(4n) { animation-delay: .36s; }
@keyframes wv { 0%,100% { height: 22%; } 50% { height: 100%; } }
.rec-time { font-variant-numeric: tabular-nums; font-size: 13px; color: var(--err); font-weight: 600; }
.rec-actions { display: flex; gap: 8px; }
.mini { border: 1px solid var(--line); background: var(--panel2); color: var(--fg);
border-radius: 8px; padding: 5px 11px; font-size: 12px; cursor: pointer; }
.mini.ok { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
.mini.cancel { color: var(--err); }
.asr-note { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--dim);
margin-bottom: 8px; padding-left: 2px; }
.asr-note .live { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
/* === 005 · выбор ассистента (agent-opt) === */
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
.agent-opt { display: flex; align-items: center; gap: 12px; padding: 11px 12px;
border-radius: 11px; background: var(--panel2); margin-bottom: 9px; cursor: pointer;
border: 1.5px solid transparent; }
.agent-opt:hover { border-color: #33414F; }
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
.agent-opt .pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%;
border: 2px solid #3D4A58; }
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
.agent-opt.on .pick::after { content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px;
height: 9px; border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg); }
.agent-opt .ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
.agent-opt .ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.agent-opt .ag-state.ok { color: var(--ok); }
.agent-opt .ag-state.off { color: var(--dim); } .agent-opt .ag-state.off .d { background: var(--dim); }
.agent-opt .ag-state.err { color: var(--err); } .agent-opt .ag-state.err .d { background: var(--err); }
/* === 006 · формы групп (approved-вариант) === */
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
.warn { display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
font-size: 12px; color: #F0A6AA; line-height: 1.5; }
.warn .i { flex: 0 0 auto; font-size: 13px; }
.info { display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
font-size: 11.5px; color: var(--dim); line-height: 1.5; }
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
.grp { display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent; }
.grp:hover { border-color: var(--line); }
.grp .mid { flex: 1; min-width: 0; }
.grp .nm { font-size: 14px; font-weight: 550; }
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
.grp.sys { background: transparent; border: 1px dashed var(--line); }
.grp.sys .nm { color: var(--fg); }
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit; }
.field input:focus { border-color: var(--accent); }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }