diff --git a/BORROW-FROM-ASSISTENT.md b/BORROW-FROM-ASSISTENT.md index 9d4305a..b0b9eea 100644 --- a/BORROW-FROM-ASSISTENT.md +++ b/BORROW-FROM-ASSISTENT.md @@ -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/*` | diff --git a/MARKDOWN-SOURCE.md b/MARKDOWN-SOURCE.md index 79256fd..aa89bd9 100644 --- a/MARKDOWN-SOURCE.md +++ b/MARKDOWN-SOURCE.md @@ -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`. -Единственная внешняя зависимость — библиотека парсера `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` (в `...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`. diff --git a/MIC-ASR-SEARCH.md b/MIC-ASR-SEARCH.md index 5e7d662..05a084b 100644 --- a/MIC-ASR-SEARCH.md +++ b/MIC-ASR-SEARCH.md @@ -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 +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//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`) — закрывать ровно один раз. + diff --git a/README.md b/README.md index 1d75fef..126fce7 100644 --- a/README.md +++ b/README.md @@ -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. Где показывать несколько агентов в одном списке диалогов (сейчас список — + у активного агента). diff --git a/THEMES.md b/THEMES.md index 5bc88d3..08b81f3 100644 --- a/THEMES.md +++ b/THEMES.md @@ -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`), иначе не читается. + ## Что осталось решить - **Какие темы будут кроме тёмной.** Пока известно только: тёмная есть, светлая diff --git a/approved/001-sidebar-utility.html b/approved/001-sidebar-utility.html deleted file mode 100644 index 1420669..0000000 --- a/approved/001-sidebar-utility.html +++ /dev/null @@ -1,413 +0,0 @@ - - - - -Вариант 1 — Sidebar / Utility - - - -
- - - - -
-
- -
АГ
-
-
agentik · рабочий
-
● на связи
-
- - - -
- -
-
Проверь, почему падает CI в agentik14:18
- -
- Думает: 42 прогона, красных 40. Смотрю имена тестов… похоже на проблему кодировки при создании файлов классов. -
- -
⚙ grep -rnP 'fun `[^`]*[^\x00-\x7F]' --include='*.kt'
- -
Нашёл причину: в именах тестов стоит длинное тире «—», а сборка работает в ASCII-кодировке. Отсюда падение компилятора. Починил, CI зелёный.14:22
- -
Отлично. Сделай релиз «3»14:23
- -
- Думает: создаю тег, запускаю публикацию… проверю, что в Nexus появилась версия 3. -
-
- - -
-
- -
- распознаю: -
- -
- -
- 0:00 -
- -
-
- -
-
- -
- - -
- -
- -
-
-
-
- - - - diff --git a/approved/004-settings-agents.html b/approved/004-settings-agents.html deleted file mode 100644 index 7e9b35c..0000000 --- a/approved/004-settings-agents.html +++ /dev/null @@ -1,404 +0,0 @@ - - - - -Одобрено: Агенты — список, форма, проверка связи - - - - - -
-
-

Агенты

- -
- - -
-
-
АГ
-
-
agentik · рабочий
-
http://192.168.88.135:8080/agentik
-
-
на связи
-
- - - -
-
- -
-
ДМ
-
-
домашний
-
http://192.168.76.199:8080/agentik
-
-
не отвечает
-
- - - -
-
- -
-
-
-
Кот-ассистент
-
http://192.168.88.24:8080/agentik
-
-
не проверялся
-
- - - -
-
- - -
- - - - -
- -
- -
- -
- - -
-
-
-

Проверка связи

- -
- -
-
-
-
-
Спрашиваю агента…
-
http://192.168.88.135:8080/agentik
-
-
- - - - -
- -
- - -
-
-
- - - - diff --git a/approved/005-new-chat-picker.html b/approved/005-new-chat-picker.html deleted file mode 100644 index 6d615e4..0000000 --- a/approved/005-new-chat-picker.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - -Одобрено: Новый диалог — выбор ассистента - - - - -
-
-

Новый диалог

- -
- -
-
В каком ассистенте создать диалог?
- -
-
АГ
-
-
agentik · рабочий
-
http://192.168.88.135:8080/agentik
-
на связи
-
-
-
- -
-
ДМ
-
-
домашний
-
http://192.168.76.199:8080/agentik
-
на связи
-
-
-
- -
-
-
-
Кот-ассистент
-
http://192.168.88.24:8080/agentik
-
не отвечает
-
-
-
- -
- ⚠ - Ассистент сейчас не отвечает. Диалог создать можно, но - сообщение не уйдёт, пока он не поднимется. -
-
- -
- - -
-
- - - - diff --git a/approved/006-groups-manage/1-list.html b/approved/006-groups-manage/1-list.html deleted file mode 100644 index abb9b2c..0000000 --- a/approved/006-groups-manage/1-list.html +++ /dev/null @@ -1,296 +0,0 @@ - - - - -Группы — список - - - - -
- - -
-
-
АГ
-
-
agentik · рабочий
-
● на связи
-
-
-
-
Проверь, почему падает CI
-
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
-
-
- -
-
-
-
-

Группы

- -
-
-
-
-
Все чаты
-
Показывает все диалоги, из любой группы
-
-
встроенная
-
- -
-
-
Работа
-
12 диалогов
-
-
- - -
-
- -
-
-
Дом
-
4 диалога
-
-
- - -
-
- -
-
-
Учёба
-
2 диалога
-
-
- - -
-
- -
-
-
Личное
-
пустая
-
-
- - -
-
- - -
-
- -
-
-
-
- - diff --git a/approved/006-groups-manage/2-add.html b/approved/006-groups-manage/2-add.html deleted file mode 100644 index e1d10ee..0000000 --- a/approved/006-groups-manage/2-add.html +++ /dev/null @@ -1,255 +0,0 @@ - - - - -Группы — новая - - - - -
- - -
-
-
АГ
-
-
agentik · рабочий
-
● на связи
-
-
-
-
Проверь, почему падает CI
-
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
-
-
- -
-
-
-
-

Новая группа

- -
-
-
- - -
Так группа будет называться в ленте над списком диалогов.
-
- -
- ℹ - Диалоги в группу попадают потом — из самого диалога. Здесь группа - только создаётся, диалог в неё не переносится. -
-
-
- - -
-
-
-
- - diff --git a/approved/006-groups-manage/3-edit.html b/approved/006-groups-manage/3-edit.html deleted file mode 100644 index 303e4e1..0000000 --- a/approved/006-groups-manage/3-edit.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - -Группы — изменение - - - - -
- - -
-
-
АГ
-
-
agentik · рабочий
-
● на связи
-
-
-
-
Проверь, почему падает CI
-
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
-
-
- -
-
-
-
-

Группа «Работа»

- -
-
-
- - -
Так группа будет называться в ленте над списком диалогов.
-
- -
- ℹ - В группе сейчас 12 диалогов. Переименование их не затрагивает — - это только название группы. -
- -
-
Опасное
- -
-
-
- - -
-
-
-
- - diff --git a/approved/006-groups-manage/4-delete.html b/approved/006-groups-manage/4-delete.html deleted file mode 100644 index e8414dd..0000000 --- a/approved/006-groups-manage/4-delete.html +++ /dev/null @@ -1,267 +0,0 @@ - - - - -Группы — удаление - - - - -
- - -
-
-
АГ
-
-
agentik · рабочий
-
● на связи
-
-
-
-
Проверь, почему падает CI
-
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
-
-
- -
-
-
-
-

Удалить группу?

- -
-
-
- ⚠ - Группа «Работа» исчезнет из ленты. Диалоги не удалятся — - они останутся и будут видны в «Все чаты». Вернуть группу можно, создав её - заново, но диалоги придётся разложить руками. -
- -
- -
-
-
Оставить без группы
-
12 диалогов вернутся в «Все чаты»
-
-
✓
-
-
- -
- ℹ - Другого пути нет: диалог обязан быть где-то виден. Отдельной - «корзины» у диалогов нет — удаляются только диалоги, и не отсюда. -
-
-
- - -
-
-
-
- - diff --git a/approved/006-groups-manage/README.md b/approved/006-groups-manage/README.md deleted file mode 100644 index 2e8edba..0000000 --- a/approved/006-groups-manage/README.md +++ /dev/null @@ -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) - -**Главное, что говорит окно: диалоги не удалятся.** Группа исчезнет из ленты, -диалоги останутся и будут видны в «Все чаты». Это первое, что написано -в предупреждении, — потому что именно этого боятся, нажимая «удалить». - -Отдельный вопрос — **что сделать с диалогами**: оставить без группы (вернутся -в «Все чаты»). Другого пути нет: диалог обязан быть где-то виден, «корзины» -у диалогов не существует. Так и написано в окне — чтобы не выглядело, будто -выбор есть, а его нет. - -Кнопка подтверждения — **красная залитая**, в тексте прямо «Удалить группу». - ---- - -## Что решено и почему - -- **Карандаш вместо «+».** Иначе две кнопки подряд означают «создать» — и в шапке - списка, и в ленте групп. -- **«Все чаты» не удаляется.** Нужна точка опоры, где собран весь список. -- **Диалоги не удаляются вместе с группой.** Иначе одно нажатие стирает работу. -- **Группа без диалогов допустима.** Создать заранее — нормальный сценарий. -- **Никаких настроек у группы, кроме названия.** Цвет, значок, порядок — не - просили, не делаем. - -## Открытые вопросы - -- **Куда диалог кладётся при создании** — в текущую открытую группу или без - группы? Сейчас в макете не показано. -- **Можно ли диалог быть в нескольких группах** (как ярлык в почте) или только - в одной? От этого зависит, что писать в удалении. -- **Порядок групп** — по алфавиту, по времени создания или перетаскиванием? -- **Что если удалить группу, в которой открыт диалог.** Сейчас не показано. diff --git a/approved/README.md b/approved/README.md deleted file mode 100644 index a94f7e0..0000000 --- a/approved/README.md +++ /dev/null @@ -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` — четыре файла, открывать по одному, по состоянию. diff --git a/sketches/002-rail-voice-first/README.md b/sketches/002-rail-voice-first/README.md deleted file mode 100644 index 24604f9..0000000 --- a/sketches/002-rail-voice-first/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Вариант 2 — Rail / Voice-first - -## Суть -Голос — главное действие, а не дополнение к клавиатуре. Огромная кнопка микрофона -слева в панели ввода, живая расшифровка на экране, пока говоришь. - -## Ключевые решения -- **Раскладка:** рейка иконок (76) + диалоги (296) + чат. Рейка — вход в память агента - и инструменты, это не просто мессенджер. -- **Голос:** кнопка 58 px, при записи краснеет и пульсирует. Над полем раскрывается - панель: волна, таймер и **текст, который появляется по мере речи**. -- **Три выхода из записи:** «Отправить» / «В поле ввода» (поправить руками) / «Отменить». - Голосовое сообщение в ленте рисуется с волной и кнопкой проигрывания. -- **Горячие клавиши:** Ctrl+R — запись, удержание пробела — говорить, Esc — отмена. - -## Что хорошо -- Голосом пользоваться реально удобно: видно, что тебя услышали, до отправки. -- Ошибку в тексте видно до отправки — можно поправить. - -## Что слабо -- Больше элементов на экране, надо разбираться. -- «Пробел на удержание» конфликтует с обычным набором текста — нужна аккуратность. - -## Кому -Тому, кто в основном диктует, а не печатает. diff --git a/sketches/002-rail-voice-first/index.html b/sketches/002-rail-voice-first/index.html deleted file mode 100644 index b041e3a..0000000 --- a/sketches/002-rail-voice-first/index.html +++ /dev/null @@ -1,342 +0,0 @@ - - - - -Вариант 2 — Rail / Voice-first - - - -
- - - - -
-
-

Диалоги

-

локальный агент · 3 непрочитанных

-
- -
-
-
АГ
-
-
agentik14:22
-
Собрал отчёт, жду правок
-
-
-
-
ОЧ
-
-
Отчёт за квартал12:05
-
Файл лежит на диске
-
-
-
-
РЗ
-
-
Разбор логоввчера
-
Нашёл 3 ошибки 502
-
-
-
-
ПО
-
-
Solana-бэкендвчера
-
Девайс-нет стартовал
-
-
-
-
КН
-
-
Книгипн
-
Разложил по полкам
-
-
-
-
- - -
-
-
-
agentik
-
на связи · codding-big
-
-
- - -
-
- -
-
Посмотри логи за вчера14:10
-
Проверил: 3 ошибки 502 около 04:20, все — обрыв соединения с upstream. Виновник — таймаут прокси. Поправить?14:12
- -
- -
- 0:07 -
- -
Понял: увеличить таймаут до 60 секунд и перезапустить прокси. Сделал — прокси поднят заново, ошибок за последний час нет.14:20
-
- -
-
-
ЗАПИСЬ · 0:00
-
-
-
- - - -
-
- -
- -
-
Говорите — текст появится в поле ниже. Можно править перед отправкой.
-
- - -
-
-
- -
- Ctrl+R — начать/стоп · Пробел (удержание) — говорить · Enter — отправить · Esc — отменить запись -
-
-
-
- - - - diff --git a/sketches/003-three-pane-command/README.md b/sketches/003-three-pane-command/README.md deleted file mode 100644 index 93d444a..0000000 --- a/sketches/003-three-pane-command/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Вариант 3 — Three-pane / Command center - -## Суть -Три панели: диалоги, чат и **состояние агента**. Работа агента видна не как текст -в переписке, а как отдельные раскрывающиеся шаги. - -## Ключевые решения -- **Раскладка:** 268 + центр + 306. Третья панель — модель, контекст, инструменты, - память агента, файлы сессии. -- **Шаг агента** — сворачиваемый блок: что делал, сколько заняло, детали по клику. - Это то, чем агент отличается от собеседника, и здесь оно вынесено в структуру. -- **Кнопка записи:** компактная рядом с «Отправить»; при записи снизу появляется - строка: квадрат «стоп», индикатор уровня, текущая расшифровка, таймер. -- **Короткая диктовка** (от 2 с) уходит сразу, текст остаётся в поле. - -## Что хорошо -- Видно, что агент делает прямо сейчас и сколько это стоит. -- Удобно на широком экране, ничего не надо прятать. - -## Что слабо -- На узком экране (ноутбук 1366) третью панель придётся убирать. -- Настройки сессии — не то, на что хочется смотреть постоянно. - -## Кому -Тому, кто держит агента включённым весь день и следит за расходом. diff --git a/sketches/003-three-pane-command/index.html b/sketches/003-three-pane-command/index.html deleted file mode 100644 index 0883251..0000000 --- a/sketches/003-three-pane-command/index.html +++ /dev/null @@ -1,345 +0,0 @@ - - - - -Вариант 3 — Three-pane / Command center - - - -
-
-
-
agentik-desktop — agentik · рабочий
-
- агент на связи - Память: 128 заметок -
-
- -
- - - - -
-
-
-
agentik · рабочий
-
codding-big · контекст 34%
-
-
-
- -
-
Разберись, почему CI красный, и выпусти релиз 314:05
- -
-
🔍Поиск причины - готово · 4 с▼
-
Нашёл: в именах тестов длинное тире, сборка идёт в ASCII.
- InvalidPathException: unmappable characters
-
- -
-
✎Правка 16 имён тестов - готово · 12 с▼
-
Заменено на обычный дефис в 4 файлах. Ничего другого не тронуто.
-
- -
-
⚙Сборка и проверка - готово · 1 м 10 с▼
-
14 модулей, тесты зелёные, jar-ы собраны.
-
- -
Готово. CI зелёный, релиз «3» опубликован: 101 артефакт в Nexus.14:22
-
- -
-
- -
- слушаю… - 0:00 - -
- -
-
- - -
-
- Ctrl+R запись - Enter отправить - Shift+Enter перенос -
-
-
- - - -
-
- - - - diff --git a/sketches/004-settings-agents/README.md b/sketches/004-settings-agents/README.md deleted file mode 100644 index ec00c34..0000000 --- a/sketches/004-settings-agents/README.md +++ /dev/null @@ -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. - -## Плюс: список диалогов с двумя агентами -Диалоги сгруппированы по агентам, у группы цветная точка, у каждой строки цветная -полоска, цвет аватара совпадает с цветом агента из настроек. - -## Что слабо / открытые вопросы -- Одного цвета для различения может оказаться мало. -- Где хранится список агентов — не решено. -- Пароля/ключа доступа у библиотеки нет — поля для него нет. diff --git a/sketches/004-settings-agents/index.html b/sketches/004-settings-agents/index.html deleted file mode 100644 index e5b7d64..0000000 --- a/sketches/004-settings-agents/index.html +++ /dev/null @@ -1,625 +0,0 @@ - - - - -Настройки: агенты - - - - -
- - - - - -
- -
- - - -
-
-
АГ
-
-
agentik · рабочий
-
● на связи
-
- -
-
-
Проверь, почему падает CI
-
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
-
-
- - -
- - -
-
-
-

Агенты

- -
- - -
-
-
АГ
-
-
agentik · рабочий
-
http://192.168.88.135:8080/agentik
-
-
на связи
-
- - -
-
- -
-
ДМ
-
-
домашний
-
http://192.168.76.199:8080/agentik
-
-
не отвечает
-
- - - -
-
- - -
- - - - -
- -
- -
-
- - -
- - -
-
-
-

Проверка связи

- -
- -
-
-
-
-
Спрашиваю агента…
-
http://192.168.88.135:8080/agentik
-
-
- - - - -
- -
- - -
-
-
-
- - - - diff --git a/sketches/005-new-chat-picker/README.md b/sketches/005-new-chat-picker/README.md deleted file mode 100644 index e957306..0000000 --- a/sketches/005-new-chat-picker/README.md +++ /dev/null @@ -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`), окно и затемнение устроены так же. Ничего нового не изобреталось. diff --git a/sketches/005-new-chat-picker/index.html b/sketches/005-new-chat-picker/index.html deleted file mode 100644 index 4673b4e..0000000 --- a/sketches/005-new-chat-picker/index.html +++ /dev/null @@ -1,436 +0,0 @@ - - - - -Новый диалог — выбор ассистента - - - - -
- - - - - -
- -
- - - - -
-
-
АГ
-
-
agentik · рабочий
-
● на связи
-
- -
-
-
Проверь, почему падает CI
-
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
-
-
- -
- - -
-
-
-

Новый диалог

- -
- -
-
В каком ассистенте создать диалог?
- -
-
-
АГ
-
-
agentik · рабочий
-
http://192.168.88.135:8080/agentik
-
на связи
-
-
-
- -
-
ДМ
-
-
домашний
-
http://192.168.76.199:8080/agentik
-
на связи
-
-
-
- -
-
-
-
Кот-ассистент
-
http://192.168.88.24:8080/agentik
-
не отвечает
-
-
-
-
- -
- ⚠ - Ассистент сейчас не отвечает. Диалог создать можно, но - сообщение не уйдёт, пока он не поднимется. -
-
- -
- - -
-
-
-
- - - - diff --git a/sketches/README.md b/sketches/README.md new file mode 100644 index 0000000..6637ecf --- /dev/null +++ b/sketches/README.md @@ -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` | Выбор диалога для добавления в группу: поиск + чекбоксы по диалогам | + +## Как смотреть + +Открыть файл в браузере — самодостаточный, без сборки и без зависимостей. diff --git a/sketches/index.html b/sketches/index.html new file mode 100644 index 0000000..b71e135 --- /dev/null +++ b/sketches/index.html @@ -0,0 +1,1426 @@ + + + + +Agentik desktop — все экраны + + + + + + +
+
1 · список диалогов рейка / voice-first, диалог не выбран
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+ +
К
+
+
Книгипн
+
Разложил по полкам
+
+
+
+ +
О
+
+
Отчёт за квартал12:05
+
Файл лежит на диске1
+
+
+
+ +
Р
+
+
Разбор логоввчера
+
Нашёл 3 ошибки 502
+
+
+
+ +
S
+
+
Solana-бэкендвчера
+
Девайс-нет стартовал
+
+
+
+ +
Д
+
+
Деплой в продпн
+
Готово, релиз 3 опубликован
+
+
+
+
+
+
+
💬
+

Выберите диалог слева

+

Или нажмите + в шапке списка, чтобы создать новый. Пока ни один диалог не открыт.

+
+
+
+
+ + +
+
2 · открытый чат диалог с агентом, последний ход агента завершён
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+ +
К
+
+
Книгипн
+
Разложил по полкам
+
+
+
+ +
О
+
+
Отчёт за квартал12:05
+
Файл лежит на диске1
+
+
+
+ +
Р
+
+
Разбор логоввчера
+
Нашёл 3 ошибки 502
+
+
+
+
+
+
+
С
+
+
agentik · рабочий
+
на связи
+
+
+ +
+
+
+
Посмотри логи за вчера14:10
+
Проверил: 3 ошибки 502 около 04:20, все — обрыв соединения с upstream. Виновник — таймаут прокси. Поправить?14:12
+
Да, увеличь таймаут до 60 и перезапусти прокси14:14
+
+ ⚙ + вызов инструмента: shell + ▾ +
+
Готово: таймаут поднят, прокси стартанул, за последний час ошибок нет.14:22
+
+
+
+
Сообщение агенту…
+ + +
+
+
+
+
+ + +
+
3 · запись голоса композер в режиме recording — панель с волной и таймером
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+
+
+
+
С
+
+
agentik · рабочий
+
на связи
+
+
+
+
Тогда профиль лучше не ставить — массив «дышит». Возьмите герметик для дерева: он остаётся эластичным.14:33
+
Скинь, что брать по смесителю14:34
+
Смеситель с выдвижным изливом, GROHE или Hansgrohe.14:35
+
+
+
+
+
0:07
+ + + +
+
+
+
+
+ + +
+
4 · нет ассистентов пустое состояние рейки и основной зоны
+
+
+

Диалоги

+
+
Все чаты
+
Работа
+ +
+
🔍 Поиск…
+
+
+
+
+
💬
+

Диалогов пока нет

+

Добавьте первого ассистента — он появится в списке, и можно будет начать.

+ +
+
+
+
+ + +
+
5 · модалка нового диалога выбор агента для нового диалога
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок
+
+
+
+
+
+
+
💬
+

Выберите диалог слева

+

Или нажмите + в шапке списка, чтобы создать новый.

+
+
+
+ +
+
+
+ + +
+
6 · управление группами модалка со списком групп + добавить
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+ +
К
+
+
Книгипн
+
Разложил по полкам
+
+
+
+
+
+
+
С
+
+
agentik · рабочий
+
на связи
+
+
+
+
Тогда профиль лучше не ставить — массив «дышит». Возьмите герметик для дерева.14:33
+
+
+
+ +
+
+
+ + +
+
7 · создание группы форма новой группы + выбор диалогов
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+ +
К
+
+
Книгипн
+
Разложил по полкам
+
+
+
+
+
+
+
С
+
+
agentik · рабочий
+
на связи
+
+
+
+
Тогда профиль лучше не ставить — массив «дышит». Возьмите герметик для дерева.14:33
+
+
+
+ +
+
+
+ + +
+
8 · изменение группы переименование, диалоги, опасная зона
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+
+
+
+
С
+
+
agentik · рабочий
+
на связи
+
+
+
+
Тогда профиль лучше не ставить — массив «дышит». Возьмите герметик для дерева.14:33
+
+
+
+ +
+
+
+ + +
+
9 · выбор диалога для группы список диалогов с галочками
+
+
+

Диалоги

+
+
Все чаты3
+
Работа
+
Дом
+ +
+
🔍 Поиск…
+
+
+ +
С
+
+
agentik · рабочий14:22
+
Собрал отчёт, жду правок2
+
+
+
+
+
+
+
С
+
+
agentik · рабочий
+
на связи
+
+
+
+
Тогда профиль лучше не ставить — массив «дышит». Возьмите герметик для дерева.14:33
+
+
+
+ +
+
+
+ + +
+
10 · агенты · список окно «Агенты»: карточки + действия
+
+
+

Агенты

+ +
+
+
+
АГ
+
+
agentik
+
Корпоративный помощник: код, документы, отчёты
+
http://192.168.88.135:8080/agentik
+
+
на связи
+
+ + + +
+
+
+
ДМ
+
+
home-assistant
+
Рецепты, расписания, бытовые мелочи
+
http://192.168.76.199:8080/agentik
+
+
не отвечает
+
+ + + +
+
+
+
+
+
Кот-ассистент
+
Поддержка и шутки в трудную минуту
+
http://192.168.88.24:8080/agentik
+
+
не проверялся
+
+ + + +
+
+ +
+
+ +
+
+
+ + +
+
11 · агенты · новый форма добавления: hint про AgentInfo, адрес, цвет/картинка
+
+
+

Новый агент

+ +
+
+
+ Имя и описание берутся с сервера агента автоматически + (AgentInfo.name, description, usefulness). + Здесь нужно только настроить, как отличать агента визуально. +
+
+ +
+ + +
+
Адрес целиком, вместе с путём /agentik. Если агент + на этой же машине — http://localhost:8080/agentik.
+
+
+ +
+
Цвет
+
Картинка
+
+
+
+
+
+
+
+
+
+
Цветом помечаются диалоги агента в списке — полоска у строки + и точка у группы.
+
+
+
+
+ + +
+
+
+ + +
+
12 · агенты · изменить форма правки: адрес предзаполнен, имя/описание недоступны
+
+
+

Изменить агента

+ +
+
+
+ Имя и описание задаются сервером и здесь недоступны для правки. + Здесь можно изменить адрес и визуальную метку. +
+
+ +
+ + +
+
Адрес целиком, вместе с путём /agentik.
+
+
+ +
+
Цвет
+
Картинка
+
+
+
+
+
+
+
+
+
+
Цветом помечаются диалоги агента в списке.
+
+
+
+
+ + +
+
+
+ + +
+
13 · агенты · проверка связи поповер поверх затемнённого списка
+
+
+

Агенты

+ +
+
+
+
АГ
+
+
agentik
+
Корпоративный помощник: код, документы, отчёты
+
http://192.168.88.135:8080/agentik
+
+
на связи
+
+ + + +
+
+
+
+
+
+
+
+

Проверка связи

+ +
+
+
+
✓
+
+
Агент отвечает
+
http://192.168.88.135:8080/agentik
+
+
+
+
Ответ на запросok
+
Отвечает по адресуhttp://192.168.88.135:8080/agentik
+
Диалогов у агента4
+
Отклик38 мс
+
+
Адрес рабочий. Можно добавлять этого агента.
+
+
+ + +
+
+
+
+ + +
+
14 · sidebar-utility одобренный канон — сайдбар + папки + полноценный композер с ASR
+
+ + +
+
+ +
АГ
+
+
agentik · рабочий
+
● на связи
+
+ +
+ +
+
Проверь, почему падает CI в agentik14:18
+
Думает: 42 прогона, красных 40. Смотрю имена тестов… + похоже на проблему кодировки при создании файлов классов.
+
⚙ grep -rnP 'fun `[^`]*[^\x00-\x7F]' --include='*.kt'
+
Нашёл причину: в именах тестов стоит длинное тире «—», а сборка работает в ASCII-кодировке. Отсюда падение компилятора. Починил, CI зелёный.14:22
+
Отлично. Сделай релиз «3»14:23
+
Думает: создаю тег, запускаю публикацию… + проверю, что в Nexus появилась версия 3.
+
+ +
+
+
распознаю: + проверь логи сборки за вчерашний день и сделай вывод …
+ +
+ +
+ 0:12 +
+
+ +
+
+ + +
+ +
готов · голос Ctrl+R · отправка Enter
+
+
+
+
+ + +
+
15 · agent-picker · одобренный модалка выбора ассистента с warn про недоступного
+
+
+

Новый диалог

+ +
+
+
В каком ассистенте создать диалог?
+ +
+
АГ
+
+
agentik · рабочий
+
http://192.168.88.135:8080/agentik
+
на связи
+
+
+
+ +
+
ДМ
+
+
домашний
+
http://192.168.76.199:8080/agentik
+
на связи
+
+
+
+ +
+
+
+
Кот-ассистент
+
http://192.168.88.24:8080/agentik
+
не отвечает
+
+
+
+ +
+ ⚠ + Ассистент сейчас не отвечает. Диалог создать можно, но + сообщение не уйдёт, пока он не поднимется. +
+
+
+ + +
+
+
+ + +
+
16 · группы · список одобренный — модалка поверх окна, sidebar-сайдбар
+
+ +
+
+
АГ
+
agentik · рабочий
● на связи
+
+
+
Проверь, почему падает CI
+
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
+
+
+
+
+
+

Группы

+
+
+
Все чаты
Показывает все диалоги, из любой группы
+
встроенная
+
+
+
Работа
12 диалогов
+
+ + +
+
+
+
Дом
4 диалога
+
+ + +
+
+
+
Учёба
2 диалога
+
+ + +
+
+
+
Личное
пустая
+
+ + +
+
+ +
+
+
+
+
+
+ + +
+
17 · группы · новая одобренный — форма без выбора диалогов (диалоги — потом)
+
+ +
+
+
АГ
+
agentik · рабочий
● на связи
+
+
+
Проверь, почему падает CI
+
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
+
+
+
+
+
+

Новая группа

+
+
+ + +
Так группа будет называться в ленте над списком диалогов.
+
+
+ ℹ + Диалоги в группу попадают потом — из самого диалога. Здесь группа + только создаётся, диалог в неё не переносится. +
+
+
+ + +
+
+
+
+
+ + +
+
18 · группы · изменение одобренный — переименование + danger-zone в той же форме
+
+ +
+
+
АГ
+
agentik · рабочий
● на связи
+
+
+
Проверь, почему падает CI
+
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
+
+
+
+
+
+

Группа «Работа»

+
+
+ + +
Так группа будет называться в ленте над списком диалогов.
+
+
+ ℹ + В группе сейчас 12 диалогов. Переименование их не затрагивает — + это только название группы. +
+
+
Опасное
+ +
+
+
+ + +
+
+
+
+
+ + +
+
19 · группы · удаление одобренный — отдельная модалка подтверждения с warn + красный CTA
+
+ +
+
+
АГ
+
agentik · рабочий
● на связи
+
+
+
Проверь, почему падает CI
+
Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.
+
+
+
+
+
+

Удалить группу?

+
+
+ ⚠ + Группа «Работа» исчезнет из ленты. Диалоги не удалятся — + они останутся и будут видны в «Все чаты». Вернуть группу можно, создав её + заново, но диалоги придётся разложить руками. +
+
+ +
+
+
Оставить без группы
+
12 диалогов вернутся в «Все чаты»
+
+
✓
+
+
+
+ ℹ + Другого пути нет: диалог обязан быть где-то виден. Отдельной + «корзины» у диалогов нет — удаляются только диалоги, и не отсюда. +
+
+
+ + +
+
+
+
+
+ + + \ No newline at end of file diff --git a/sketches/style.css b/sketches/style.css new file mode 100644 index 0000000..97eb792 --- /dev/null +++ b/sketches/style.css @@ -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; } \ No newline at end of file