Макеты — один sketches/index.html; approved/ удалён
- sketches/index.html: все экраны одним документом в порядке 1·… (14–19 — бывшие approved) - approved/ (4 файла групп + 3 макета + README) удалён как дубль - стили вынесены в sketches/style.css - README, THEMES, BORROW-FROM-ASSISTENT, MARKDOWN-SOURCE, MIC-ASR-SEARCH приведены к фактическому состоянию
This commit is contained in:
@@ -78,7 +78,9 @@ compose.desktop {
|
||||
|
||||
## 2. Отрисовка Markdown
|
||||
|
||||
Отдельный документ: **`MARKDOWN-SOURCE.md`** — 4 файла, разобрано подробно.
|
||||
**Уже скопировано в проект** (28.09.2026): 6 файлов + 11 тестов парсера, пакеты
|
||||
переименованы под `pw.binom.agentik.desktop.*`. Подробности, карта «источник →
|
||||
назначение», зависимости и как пересинхронизировать — в **`MARKDOWN-SOURCE.md`**.
|
||||
|
||||
## 3. Приёмы отрисовки (самое ценное)
|
||||
|
||||
@@ -343,7 +345,7 @@ val DarkClientTheme = ClientTheme(
|
||||
| Берём | Не берём |
|
||||
|---|---|
|
||||
| Версии и плагины, репозитории | Архитектуру и слои |
|
||||
| Отрисовку Markdown (4 файла) | Экраны целиком (`ChatScreen`, `SettingsDialog`, …) |
|
||||
| Отрисовку Markdown (6 файлов, уже скопированы) | Экраны целиком (`ChatScreen`, `SettingsDialog`, …) |
|
||||
| Приёмы: таблицы, ленивый список, автоскролл, полоса прокрутки | Модули `chat-*` и модели сообщений |
|
||||
| Кликабельные ссылки, копирование кода, выделение текста | `MessageBubble` |
|
||||
| Приём записи звука (формат 16к/моно, `javax.sound`) | `ChatViewModel`, `config/*`, `cache/*` |
|
||||
|
||||
+98
-112
@@ -1,150 +1,136 @@
|
||||
# Откуда брать отрисовку Markdown
|
||||
# Отрисовка Markdown: откуда взято и где лежит
|
||||
|
||||
Готовый рендер Markdown для Compose уже написан и лежит в репозитории **`ai/assistent`**.
|
||||
Писать своё не надо — надо взять это.
|
||||
**Статус: УЖЕ СКОПИРОВАНО В ПРОЕКТ (28.09.2026).** Код вендорен в `src/`,
|
||||
собирается, тесты парсера (11 шт.) зелёные. Своё писать не надо.
|
||||
|
||||
**Репозиторий:** https://git.binom.pw/ai/assistent (ветка `main`)
|
||||
**Проверено на коммите:** `2c5b3f9` (файлы добавлены в `1acced4`, 2026-08-13)
|
||||
**Важно:** репозиторий приватный. Браузерная ссылка откроется только у залогиненного;
|
||||
для копирования файлов из скрипта — RAW-адреса с логином/паролем (ниже).
|
||||
Источник — приватный репозиторий **`ai/assistent`** (https://git.binom.pw/ai/assistent,
|
||||
ветка `main`, проверено на коммите `2c5b3f9`, файлы добавлены в `1acced4` 2026-08-13).
|
||||
Пакеты переименованы под наш проект; внешняя зависимость — одна
|
||||
(`ru.otpbank.ai:markdown`).
|
||||
|
||||
---
|
||||
|
||||
## Что именно копировать: 4 файла
|
||||
## Куда что легло
|
||||
|
||||
Всё лежит в двух модулях. Модуль `client-shared` — общий, собирается под **JVM + Compose**
|
||||
(то есть ровно наш случай, десктоп), не под Android.
|
||||
| В assistent | У нас |
|
||||
|---|---|
|
||||
| `client-shared/.../shared/markdown/MarkdownSegment.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/markdown/MarkdownSegment.kt` |
|
||||
| `client-shared/.../shared/markdown/MarkdownParserImpl.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/markdown/MarkdownParserImpl.kt` |
|
||||
| `client-shared/.../shared/ui/MarkdownRendering.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt` |
|
||||
| `client-shared/.../shared/ui/ChatImage.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt` |
|
||||
| `client-shared/.../shared/ClientImageSupport.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ClientImageSupport.kt` |
|
||||
| `client/.../client/ui/MarkdownRenderer.kt` | `src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRenderer.kt` |
|
||||
| `client-shared/src/test/.../markdown/MarkdownParserTest.kt` | `src/jvmTest/kotlin/pw/binom/agentik/desktop/markdown/MarkdownParserTest.kt` |
|
||||
|
||||
### 1. Модель данных — `MarkdownSegment.kt` (59 строк)
|
||||
Переименование пакетов:
|
||||
|
||||
```
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt
|
||||
```
|
||||
- `pw.binom.client.shared.markdown` → `pw.binom.agentik.desktop.markdown`
|
||||
- `pw.binom.client.shared.ui` → `pw.binom.agentik.desktop.ui`
|
||||
- `pw.binom.client.shared` (`ClientImageSupport`) → `pw.binom.agentik.desktop.ui`
|
||||
- `pw.binom.client.ui` (`MarkdownRenderer`) → `pw.binom.agentik.desktop.ui`
|
||||
|
||||
Что внутри: типы кусков разметки — текст, блок кода, таблица, картинка, разделитель.
|
||||
Плюс описание стилевого участка: жирный, курсив, код, зачёркнутый, ссылка.
|
||||
Зависимостей от Compose нет вообще — чистые данные.
|
||||
### Почему файлов шесть, а не четыре
|
||||
|
||||
### 2. Парсер — `MarkdownParserImpl.kt` (353 строки)
|
||||
Изначально в доке значились 4 файла. При сборке выяснилось, что
|
||||
`MarkdownRendering.kt` для сегментов-картинок вызывает `ChatImage(...)`, а тот —
|
||||
`ClientImageSupport`. Поэтому взяты ещё `ChatImage.kt` и `ClientImageSupport.kt`.
|
||||
Оба независимы от `chat-*` и `MessageBubble`.
|
||||
|
||||
```
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt
|
||||
```
|
||||
|
||||
Точка входа: `parseMarkdown(markdown: String): List<MarkdownSegment>`.
|
||||
Единственная внешняя зависимость — библиотека парсера `ru.otpbank.ai:markdown` (см. ниже).
|
||||
От Композа не зависит.
|
||||
|
||||
### 3. Отрисовка — `MarkdownRendering.kt` (377 строк)
|
||||
|
||||
```
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt
|
||||
```
|
||||
|
||||
Точка входа:
|
||||
`renderMarkdownSegments(content, textColor, fontSize, isBubbleScrolled, onUrlClick)`.
|
||||
Внутри: текст с inline-стилями, кликабельные ссылки, блоки кода с копированием,
|
||||
таблицы, картинки, разделители. Самый жирный файл и самая нужная часть.
|
||||
|
||||
### 4. Десктопная обёртка — `MarkdownRenderer.kt` (25 строк)
|
||||
|
||||
```
|
||||
client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt
|
||||
```
|
||||
|
||||
Открытие ссылок в системном браузере (`Desktop.getDesktop().browse`) и
|
||||
`parseMultiline`. Модуль `client` в assistent — это **десктопный** клиент на
|
||||
Compose Desktop, так что пример использования ровно наш.
|
||||
|
||||
---
|
||||
|
||||
## Что копировать НЕ надо
|
||||
|
||||
- `client-shared/src/main/kotlin/pw/binom/client/shared/ui/MessageBubble.kt` (789 строк) —
|
||||
пузырь сообщения целиком. Тянет за собой модуль `chat-common` (модели сообщений),
|
||||
из плюшек — выделение текста и контекстное меню. Для отрисовки разметки не нужен,
|
||||
но полезен, если понадобится пример.
|
||||
|
||||
**Границы заимствования — подробнее в `BORROW-FROM-ASSISTENT.md`.** Коротко:
|
||||
из assistent берём приёмы отрисовки, плагины и версии; архитектуру, слои и готовые
|
||||
экраны (`ChatScreen`, `SettingsDialog`, модули `chat-*`) — не берём, у нас своё.
|
||||
|
||||
---
|
||||
|
||||
## Зависимости
|
||||
|
||||
Одна, и она есть в нашем Nexus:
|
||||
`ClientImageSupport` — это реестр: платформенный клиент при старте регистрирует
|
||||
`decoder` (байты → `ImageBitmap`) и, при желании, `fetcher` и `serverBaseUrl`.
|
||||
Пока `decoder` не зарегистрирован, картинки деградируют в текст (alt/ссылку) —
|
||||
ничего не падает. На десктопе декодер регистрируется так:
|
||||
|
||||
```kotlin
|
||||
implementation("ru.otpbank.ai:markdown:0.51.0")
|
||||
```
|
||||
|
||||
Репозиторий в `build.gradle.kts` (без него не соберётся):
|
||||
|
||||
```kotlin
|
||||
maven {
|
||||
url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/")
|
||||
isAllowInsecureProtocol = true
|
||||
ClientImageSupport.decoder = { bytes ->
|
||||
org.jetbrains.skia.Image.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||
}
|
||||
```
|
||||
|
||||
Проверено: в Nexus лежит, версии с `0.1.0` по `0.51.0`, файл `markdown-0.51.0.pom`
|
||||
отдаёт `HTTP 200`.
|
||||
## Точки входа
|
||||
|
||||
Из Compose нужны: `compose.material3`, `compose.foundation`, `compose.ui`,
|
||||
`compose.materialIconsExtended` (иконка копирования в блоках кода).
|
||||
- Парсинг: `parseMarkdown(markdown: String): List<MarkdownSegment>` (в `...desktop.markdown`).
|
||||
- Отрисовка: `renderMarkdownSegments(content, textColor, fontSize, isBubbleScrolled, onUrlClick)` (в `...desktop.ui`).
|
||||
- Открытие ссылок/`parseMultiline`: `...desktop.ui.MarkdownRenderer`.
|
||||
|
||||
## Опубликован ли `client-shared` как библиотека
|
||||
## Зависимости и сборка (уже настроено)
|
||||
|
||||
**Нет.** Проверено поиском в Nexus (`pw/binom/client-shared`, `pw/binom/chat-common`) —
|
||||
`HTTP 404`. Поэтому вариантов два: либо копируем исходники к себе (4 файла выше),
|
||||
либо сперва публикуем модуль. Копирование проще.
|
||||
`gradle/libs.versions.toml`:
|
||||
|
||||
---
|
||||
```toml
|
||||
compose-multiplatform = "1.7.3" # версия из assistent; собирается на нашем Kotlin 2.4.20
|
||||
markdown = "0.51.0"
|
||||
|
||||
## Как копировать (RAW-адреса, работают с логином)
|
||||
markdown = { module = "ru.otpbank.ai:markdown", version.ref = "markdown" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
compose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||
```
|
||||
|
||||
Адрес вида `/raw/branch/main/<путь>` отдаёт файл. Проверено: `HTTP 200`, 1805 байт —
|
||||
совпадает с локальным размером.
|
||||
`build.gradle.kts` — плагины `kotlin-compose` + `compose`, зависимости
|
||||
`compose.foundation` / `compose.ui` / `compose.material3` /
|
||||
`compose.materialIconsExtended` (иконка копирования) + `libs.markdown`.
|
||||
`compose.desktop.currentOs` пока НЕ подключён — он понадобится, когда появится
|
||||
настоящее окно (`compose.desktop { application { mainClass = ... } }`), для
|
||||
компиляции рендера он не нужен.
|
||||
|
||||
`settings.gradle.kts` — репозитории (без них `ru.otpbank.ai:markdown` не найдётся):
|
||||
|
||||
```kotlin
|
||||
maven {
|
||||
name = "developerspace"
|
||||
url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/")
|
||||
setAllowInsecureProtocol(true)
|
||||
}
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
maven("https://maven.google.com")
|
||||
```
|
||||
|
||||
Проверено: `./gradlew compileKotlinJvm` — OK; `./gradlew jvmTest` —
|
||||
`MarkdownParserTest` 11/11, `ConversationMetaRepositoryIT` 14/14.
|
||||
|
||||
## Как пересинхронизировать, если в assistent что-то поправят
|
||||
|
||||
RAW-адрес: `/raw/branch/main/<путь>` (нужен логин, репозиторий приватный).
|
||||
|
||||
```bash
|
||||
BASE=https://git.binom.pw/ai/assistent/raw/branch/main
|
||||
AUTH='subochev:ПАРОЛЬ' # подставить перед запуском
|
||||
AUTH='subochev:ПАРОЛЬ'
|
||||
|
||||
for f in \
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt \
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt \
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt \
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/ui/ChatImage.kt \
|
||||
client-shared/src/main/kotlin/pw/binom/client/shared/ClientImageSupport.kt \
|
||||
client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt ; do
|
||||
curl -sS -u "$AUTH" "$BASE/$f" -o "$(basename "$f")"
|
||||
curl -sS -u "$AUTH" "$BASE/$f" -o "/tmp/$(basename "$f")"
|
||||
done
|
||||
```
|
||||
|
||||
Браузерные ссылки для чтения глазами (нужен залогиненный git.binom.pw):
|
||||
После копирования — тем же `sed`-ом переименовать пакеты (см. таблицу выше),
|
||||
`MarkdownRenderer` дополнительно сослать `renderMarkdownSegments` на
|
||||
`pw.binom.agentik.desktop.ui`.
|
||||
|
||||
```
|
||||
https://git.binom.pw/ai/assistent/src/branch/main/client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownSegment.kt
|
||||
https://git.binom.pw/ai/assistent/src/branch/main/client-shared/src/main/kotlin/pw/binom/client/shared/markdown/MarkdownParserImpl.kt
|
||||
https://git.binom.pw/ai/assistent/src/branch/main/client-shared/src/main/kotlin/pw/binom/client/shared/ui/MarkdownRendering.kt
|
||||
https://git.binom.pw/ai/assistent/src/branch/main/client/src/main/kotlin/pw/binom/client/ui/MarkdownRenderer.kt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что придётся поправить после копирования
|
||||
|
||||
1. **Пакеты.** Файлы лежат в `pw.binom.client.shared.*` — переименовать под наш проект
|
||||
(или оставить как есть, если не смущает).
|
||||
2. **`openUrl`.** В `MarkdownRenderer` открытие ссылок идёт через `java.awt.Desktop` —
|
||||
на десктопе это работает, ничего менять не надо.
|
||||
3. **Стиль.** Цвета текста и блоков кода в `MarkdownRendering` берутся из аргументов и
|
||||
`MaterialTheme` — под нашу тёмную тему подставить свои значения.
|
||||
|
||||
## Что этот рендер умеет (по коду)
|
||||
## Что рендер умеет (по коду)
|
||||
|
||||
- **Текст** с участками: жирный, курсив, код, зачёркнутый, ссылки.
|
||||
- **Блоки кода** — с названием языка и кнопкой копирования.
|
||||
- **Таблицы** — заголовки и строки, с горизонтальной прокруткой.
|
||||
- **Картинки** — `data:`-ссылки и обычные `http(s)`. Первые рисуются сразу из памяти.
|
||||
- **Блоки кода** — с названием языка и кнопкой копирования (`LocalClipboardManager`).
|
||||
- **Таблицы** — заголовки/строки, переключаются на горизонтальный скролл по
|
||||
`isBubbleScrolled` (иначе Compose падает на бесконечной maxWidth).
|
||||
- **Картинки** — `data:` и `http(s)`; рисуются через `ChatImage`/`ClientImageSupport`.
|
||||
- **Разделители** (`---`).
|
||||
- **Кликабельные ссылки** — нажатие уходит в `onUrlClick`, оттуда открываем браузер.
|
||||
- **Кликабельные ссылки** — нажатие уходит в `onUrlClick`.
|
||||
|
||||
## Известный баг на будущее
|
||||
|
||||
В assistent в `MessageBubble.kt:153` (НЕ скопирован) `hasTable` вызывает
|
||||
`parseMarkdown` вторым проходом до основной отрисовки — двойной парс на пузырь.
|
||||
У нас `MessageBubble` свой, при его написании в 0-й версии просто звать
|
||||
`renderMarkdownSegments` один раз; кэшировать разбор через
|
||||
`remember(record.id, record.text)`.
|
||||
|
||||
## Что НЕ брали
|
||||
|
||||
- `MessageBubble.kt` — тянет `chat-common` и чужие модели сообщений.
|
||||
- Модули `chat-*`, `ChatScreen`, `ChatViewModel`, `SettingsDialog` и т. п.
|
||||
- Границы заимствования — в `BORROW-FROM-ASSISTENT.md`.
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Микрофон и распознавание: результаты поиска
|
||||
|
||||
> **Статус: реализовано** (см. `src/jvmMain/.../desktop/voice/`).
|
||||
> `mic-api` + `asr-vosk` + `vad` подключены в `gradle/libs.versions.toml`,
|
||||
> кнопка микрофона в чате активна: `VoiceController` (микрофон → VAD →
|
||||
> распознавание), `VoskVoiceRecognizer` (Vosk-модель и `silero_vad.onnx` лежат
|
||||
> внутри артефактов — качать нечего). Ниже — исходный поиск, из которого это
|
||||
> выросло.
|
||||
|
||||
**Задача была:** найти готовые библиотеки для работы с микрофоном и распознавания,
|
||||
прежде чем брать код из `ai/assistent`. **Код не пишем** — только поиск.
|
||||
|
||||
@@ -172,3 +179,77 @@ client.post("$base/audio/transcriptions") { ... } // multipart, как у Open
|
||||
| Нужны ли сторонние зависимости для звука? | **Нет.** Всё на стандартной Java (`javax.sound.sampled`) и sherpa-onnx для нейронок. |
|
||||
|
||||
**Код не писан** — только поиск, как и просили.
|
||||
|
||||
---
|
||||
|
||||
## 7. Что в итоге подключено
|
||||
|
||||
| Слой | Артефакт | Где в коде |
|
||||
|---|---|---|
|
||||
| Микрофон | `pw.binom.mic:mic-api:1.0.0` | `voice/VoiceController.kt` (`MicSource` → `MicAudio.record`) |
|
||||
| Детектор речи | `pw.binom.voice.vad:vad:2` | `voice/VoiceRecognizer.kt` (`VadInstance`) |
|
||||
| Распознавание | `pw.binom.asr:asr-vosk:4` (+ `asr-api:4`) | `voice/VoiceRecognizer.kt` (`VoskVoiceRecognizer`) |
|
||||
| Сборка тракта | — | `voice/VoiceController.kt` (`VoiceState`: IDLE/PREPARING/RECORDING/RECOGNIZING) |
|
||||
| Склейка с UI | — | `ui/AppState.kt` (`voiceState`, `voicePartial`, `composerDraft`), `ui/screens/ChatPane.kt` |
|
||||
|
||||
| Прогрев до skia | — | `voice/VoiceNative.kt` (`warmUp()` из `main()`) |
|
||||
|
||||
Тесты: `voice/AudioTest.kt` (s16le→float, нарезка окон VAD + реальная загрузка
|
||||
`silero_vad.onnx`), `voice/VoiceControllerTest.kt` (фейковые микрофон и
|
||||
распознаватель), `voice/VoskVoiceRecognizerIT.kt` (настоящие Vosk + VAD на
|
||||
тишине), `voice/RealSpeechIT.kt` (настоящая русская речь после инициализации
|
||||
skia — см. ниже).
|
||||
|
||||
---
|
||||
|
||||
## 8. ⚠️ Конфликт skiko и нативных библиотек (найдено 28.09.2026)
|
||||
|
||||
**Симптом.** Приложение Compose Desktop роняется с SIGSEGV (не исключение —
|
||||
падение всего JVM) при первом нажатии микрофона. В `hs_err_pid`:
|
||||
|
||||
```
|
||||
C [libc.so.6+0x184fdc]
|
||||
C [libstdc++.so.6+0x15d820] std::ostream::_M_insert<double>
|
||||
C [jnaNNNN.tmp+0x6838e7] kaldi::ParseOptions::RegisterSpecific(...)
|
||||
j org.vosk.LibVosk.vosk_model_new(...)
|
||||
```
|
||||
|
||||
То же самое — с ONNX (`OrtSession.createSession` → `std::ostream::_M_insert`):
|
||||
C++-библиотека пишет в свой лог-стрим, а стрим оказывается невалидным.
|
||||
|
||||
**Причина — порядок загрузки.** skiko (нативный рендер Compose, грузит
|
||||
`~/.skiko/<hash>/libskiko-linux-x64.so`) и C++-стек распознавания (Vosk/Kaldi,
|
||||
onnxruntime) конфликтуют, если второй инициализируется ПОСЛЕ первого. Первое
|
||||
создание модели/сессии после инициализации skia валит JVM.
|
||||
|
||||
**Проверено пробниками (позже удалены):**
|
||||
|
||||
| Порядок | Результат |
|
||||
|---|---|
|
||||
| ONNX → skia (raster) → ONNX | ок |
|
||||
| skia (Compose-текст) → ONNX | **SIGSEGV** |
|
||||
| skia (Compose-текст) → Vosk | **SIGSEGV** |
|
||||
| **Vosk → skia (Compose-текст) → Vosk** | **ок** |
|
||||
|
||||
**Фикс.** `VoiceNative.warmUp()` вызывается в `main()` **до** `application {}`:
|
||||
создаёт и закрывает `Stt.vosk(...)` и `VadInstance(...)`. После этого и свежий
|
||||
распознаватель, и свежий VAD, созданные уже после отрисовки окна, работают.
|
||||
Побочный плюс — модель Vosk (~46 МБ, ~2 с) грузится на старте, а не при первом
|
||||
клике. Если прогрев не удался, `AppState.voiceAvailable = false` — кнопка
|
||||
микрофона заблокирована (лучше «недоступно», чем краш).
|
||||
|
||||
**Следствие для тестов.** `jvmTest` форкает JVM на каждый тест-класс
|
||||
(`forkEvery = 1`): иначе skiko-тесты (`ThemeRenderTest`) отравляют
|
||||
нативные тесты голоса в том же JVM. `RealSpeechIT` проверяет ровно
|
||||
продуктовый порядок: `warmUp()` → рендер Compose → создание распознавателя →
|
||||
распознавание реального сэмпла (`src/jvmTest/resources/voice/raz-dva-tri.wav`
|
||||
→ «раз два три четыре пять»).
|
||||
|
||||
Заметки на будущее:
|
||||
|
||||
- Создание распознавателя стоит секунды (загрузка модели Vosk), поэтому
|
||||
распаковка zip в tmp кэшируется самим `asr-vosk`, а в UI есть состояние
|
||||
«загружаю модель».
|
||||
- `VadInstance.close()` **не идемпотентен** (второй `close()` роняет JVM через
|
||||
ONNX `double free`) — закрывать ровно один раз.
|
||||
|
||||
|
||||
@@ -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. Где показывать несколько агентов в одном списке диалогов (сейчас список —
|
||||
у активного агента).
|
||||
|
||||
@@ -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`), иначе не читается.
|
||||
|
||||
## Что осталось решить
|
||||
|
||||
- **Какие темы будут кроме тёмной.** Пока известно только: тёмная есть, светлая
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Вариант 1 — Sidebar / Utility</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line);
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
/* ---------- левая колонка: список чатов ---------- */
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.icon-btn {
|
||||
width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px;
|
||||
}
|
||||
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.search {
|
||||
margin: 10px 16px 8px; padding: 8px 12px; border-radius: 9px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13px; width: calc(100% - 32px); outline: none;
|
||||
}
|
||||
.search:focus { border-color: var(--accent); }
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* лента папок — как в мобильном клиенте */
|
||||
.folders { display: flex; gap: 6px; padding: 6px 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
/* кнопка «Группы» — не папка, а действие: держим справа, отдельно от чипов */
|
||||
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
|
||||
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
|
||||
.folder.act:hover { color: var(--fg); background: var(--panel2); }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 4px 8px 12px; }
|
||||
.chat-row {
|
||||
display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start;
|
||||
}
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; } .avatar.g3 { background: #C9A886; } .avatar.g4 { background: #B79AE0; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; flex: 0 0 auto; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
||||
.badge {
|
||||
background: var(--badge); color: #0F1922; border-radius: 10px; font-size: 11px; font-weight: 700;
|
||||
padding: 1px 6px; min-width: 19px; text-align: center;
|
||||
}
|
||||
.row-tail { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: 0 0 auto; }
|
||||
|
||||
/* ---------- правая часть: чат ---------- */
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head {
|
||||
position: relative;
|
||||
height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center;
|
||||
gap: 8px; padding: 0 16px;
|
||||
}
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-meta { flex: 1; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: #63C88A; }
|
||||
|
||||
/* кнопка «назад» — нужна только в узком окне */
|
||||
.back-btn {
|
||||
display: none; width: 32px; height: 32px; flex: 0 0 32px; border-radius: 8px;
|
||||
border: none; background: transparent; color: var(--fg); cursor: pointer;
|
||||
font-size: 17px; place-items: center;
|
||||
}
|
||||
.back-btn:hover { background: var(--panel); }
|
||||
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
.msg .meta { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
|
||||
|
||||
/* блок «рассуждения» и «инструмент» — это специфика агента, не мессенджера */
|
||||
.think {
|
||||
align-self: flex-start; max-width: 80%; border-left: 2px solid var(--accent);
|
||||
background: rgba(106,178,242,.07); padding: 8px 12px; border-radius: 0 10px 10px 0;
|
||||
font-size: 12.5px; color: var(--dim); cursor: pointer;
|
||||
}
|
||||
.think b { color: var(--accent); font-weight: 600; }
|
||||
.tool {
|
||||
align-self: flex-start; max-width: 80%; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 8px 12px; font-size: 12.5px;
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
}
|
||||
.tool .gear { color: var(--accent); }
|
||||
.tool code { color: var(--dim); font-size: 11.5px; }
|
||||
|
||||
/* ---------- панель ввода с кнопкой записи ---------- */
|
||||
.composer { border-top: 1px solid var(--line); padding: 12px 16px 14px; background: var(--panel); }
|
||||
.attach-strip { display: flex; gap: 8px; margin-bottom: 9px; }
|
||||
.attach-strip:empty { display: none; }
|
||||
.chip {
|
||||
background: var(--panel2); border: 1px solid var(--line); border-radius: 8px;
|
||||
padding: 5px 9px; font-size: 11.5px; color: var(--dim); display: flex; align-items: center; gap: 7px;
|
||||
}
|
||||
.chip .x { cursor: pointer; color: var(--err); font-weight: 700; }
|
||||
|
||||
.input-row { display: flex; align-items: flex-end; gap: 9px; }
|
||||
.input-wrap { flex: 1; display: flex; align-items: flex-end; background: var(--bg); border: 1px solid var(--line);
|
||||
border-radius: 11px; padding: 9px 12px; gap: 9px; }
|
||||
.input-wrap:focus-within { border-color: var(--accent); }
|
||||
textarea {
|
||||
flex: 1; background: transparent; border: none; outline: none; resize: none; color: var(--fg);
|
||||
font-family: inherit; font-size: 13.5px; line-height: 1.45; max-height: 120px; height: 22px;
|
||||
}
|
||||
textarea::placeholder { color: var(--dim); }
|
||||
|
||||
/* круглая кнопка записи */
|
||||
.rec-btn {
|
||||
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line); background: var(--bg);
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; flex: 0 0 40px;
|
||||
transition: all .18s;
|
||||
}
|
||||
.rec-btn:hover { color: var(--fg); border-color: var(--dim); }
|
||||
.rec-btn.recording { background: var(--err); border-color: var(--err); color: #fff; animation: pulse 1.4s infinite; }
|
||||
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(236,57,66,.55); } 50% { box-shadow: 0 0 0 9px rgba(236,57,66,0); } }
|
||||
|
||||
.send-btn {
|
||||
height: 40px; padding: 0 18px; border-radius: 11px; border: none; background: var(--accent);
|
||||
color: #0F1922; font-size: 13px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.send-btn:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
|
||||
|
||||
.hint { font-size: 11px; color: var(--dim); margin-top: 8px; display: flex; justify-content: space-between; }
|
||||
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10.5px; }
|
||||
|
||||
/* запись: полоса над полем */
|
||||
.rec-panel { display: none; align-items: center; gap: 12px; margin-bottom: 10px;
|
||||
background: var(--bg); border: 1px solid var(--err); border-radius: 11px; padding: 9px 13px; }
|
||||
.rec-panel.on { display: flex; }
|
||||
.wave { display: flex; align-items: center; gap: 3px; height: 22px; flex: 1; }
|
||||
.wave i { width: 3px; background: var(--err); border-radius: 2px; height: 30%; animation: wv .9s infinite ease-in-out; }
|
||||
.wave i:nth-child(2n) { animation-delay: .12s; } .wave i:nth-child(3n) { animation-delay: .24s; }
|
||||
.wave i:nth-child(4n) { animation-delay: .36s; }
|
||||
@keyframes wv { 0%,100% { height: 22%; } 50% { height: 100%; } }
|
||||
.rec-time { font-variant-numeric: tabular-nums; font-size: 13px; color: var(--err); font-weight: 600; }
|
||||
.rec-actions { display: flex; gap: 8px; }
|
||||
.mini { border: 1px solid var(--line); background: var(--panel2); color: var(--fg); border-radius: 8px;
|
||||
padding: 5px 11px; font-size: 12px; cursor: pointer; }
|
||||
.mini.ok { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
.mini.cancel { color: var(--err); }
|
||||
|
||||
/* всплывающее состояние: расшифровка распознанного текста */
|
||||
.asr-note {
|
||||
display: none; align-items: center; gap: 8px; font-size: 11.5px; color: var(--dim);
|
||||
margin-bottom: 8px; padding-left: 2px;
|
||||
}
|
||||
.asr-note.on { display: flex; }
|
||||
.asr-note .live { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
||||
|
||||
/* ---------- узкое окно: либо список, либо чат ---------- */
|
||||
.backdrop {
|
||||
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 90;
|
||||
}
|
||||
.backdrop.on { display: block; }
|
||||
@media (max-width: 880px) {
|
||||
body { padding: 0; }
|
||||
.window { width: 100vw; height: 100vh; border-radius: 0; border: none; transform: none !important; }
|
||||
.window.mode-chat .sidebar { display: none; }
|
||||
.window.mode-list .main { display: none; }
|
||||
.sidebar { width: 100%; flex: 1 1 auto; border-right: none; }
|
||||
.back-btn { display: grid; }
|
||||
}
|
||||
|
||||
/* окно целиком влезает на любой экран — не режем картинку */
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
|
||||
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
|
||||
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window">
|
||||
<!-- ================= СПИСОК ЧАТОВ ================= -->
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="icon-btn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list" id="chatList">
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g4">ПО</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Девайс-нет стартовал</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ================= ЧАТ ================= -->
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<button class="back-btn" id="backBtn" title="Назад к диалогам">←</button>
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="head-meta">
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
|
||||
<button class="icon-btn" title="Параметры диалога">⋯</button>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="messages" id="messages">
|
||||
<div class="msg user">Проверь, почему падает CI в agentik<span class="meta">14:18</span></div>
|
||||
|
||||
<div class="think" onclick="this.querySelector('.rest').style.display = this.querySelector('.rest').style.display==='none'?'inline':'none'">
|
||||
<b>Думает:</b> 42 прогона, красных 40. Смотрю имена тестов… <span class="rest">похоже на проблему кодировки при создании файлов классов.</span>
|
||||
</div>
|
||||
|
||||
<div class="tool"><span class="gear">⚙</span> <code>grep -rnP 'fun `[^`]*[^\x00-\x7F]' --include='*.kt'</code></div>
|
||||
|
||||
<div class="msg bot">Нашёл причину: в именах тестов стоит длинное тире «—», а сборка работает в ASCII-кодировке. Отсюда падение компилятора. Починил, CI зелёный.<span class="meta">14:22</span></div>
|
||||
|
||||
<div class="msg user">Отлично. Сделай релиз «3»<span class="meta">14:23</span></div>
|
||||
|
||||
<div class="think" onclick="this.querySelector('.rest').style.display = this.querySelector('.rest').style.display==='none'?'inline':'none'">
|
||||
<b>Думает:</b> создаю тег, запускаю публикацию… <span class="rest">проверю, что в Nexus появилась версия 3.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== панель ввода ===== -->
|
||||
<div class="composer">
|
||||
<div class="attach-strip" id="chips"></div>
|
||||
|
||||
<div class="asr-note" id="asrNote">
|
||||
<span class="live"></span> распознаю: <span id="asrText" style="color:var(--fg)"></span>
|
||||
</div>
|
||||
|
||||
<div class="rec-panel" id="recPanel">
|
||||
<button class="mini cancel" onclick="stopRec(true)">✕</button>
|
||||
<div class="wave" id="wave"></div>
|
||||
<span class="rec-time" id="recTime">0:00</span>
|
||||
<div class="rec-actions">
|
||||
<button class="mini ok" onclick="stopRec(false)">Остановить и отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-row" id="inputRow">
|
||||
<div class="input-wrap">
|
||||
<textarea id="ta" rows="1" placeholder="Написать агенту…"></textarea>
|
||||
</div>
|
||||
<button class="rec-btn" id="recBtn" title="Записать голосовое (Ctrl+R)">🎙</button>
|
||||
<button class="send-btn" id="sendBtn" disabled>Отправить</button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<span id="statusHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ta = document.getElementById('ta');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const recBtn = document.getElementById('recBtn');
|
||||
const recPanel = document.getElementById('recPanel');
|
||||
const inputRow = document.getElementById('inputRow');
|
||||
const messages = document.getElementById('messages');
|
||||
const asrNote = document.getElementById('asrNote');
|
||||
const asrText = document.getElementById('asrText');
|
||||
const statusHint = document.getElementById('statusHint');
|
||||
|
||||
// волна из 28 столбиков
|
||||
document.getElementById('wave').innerHTML = Array.from({length: 28}, () => '<i></i>').join('');
|
||||
|
||||
ta.addEventListener('input', () => {
|
||||
ta.style.height = '22px';
|
||||
ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
|
||||
sendBtn.disabled = ta.value.trim() === '';
|
||||
});
|
||||
|
||||
function send() {
|
||||
const t = ta.value.trim(); if (!t) return;
|
||||
addMsg(t, 'user');
|
||||
ta.value = ''; ta.style.height = '22px'; sendBtn.disabled = true;
|
||||
statusHint.textContent = 'агент думает…';
|
||||
setTimeout(() => {
|
||||
addMsg('Принято. Выполняю: «' + t + '» — покажу ход в этом же диалоге.', 'bot');
|
||||
statusHint.textContent = 'готов';
|
||||
}, 900);
|
||||
}
|
||||
function addMsg(text, who) {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'msg ' + who;
|
||||
const now = new Date();
|
||||
d.innerHTML = text + '<span class="meta">' + now.getHours() + ':' + String(now.getMinutes()).padStart(2,'0') + '</span>';
|
||||
messages.appendChild(d); messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
sendBtn.onclick = send;
|
||||
ta.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
||||
});
|
||||
|
||||
// ---------- запись ----------
|
||||
let recTimer = null, recSec = 0;
|
||||
const WORDS = ['проверь','логи','сборки','за','вчерашний','день','и','сделай','вывод'];
|
||||
function startRec() {
|
||||
recSec = 0; recPanel.classList.add('on'); recBtn.classList.add('recording');
|
||||
inputRow.style.opacity = '.45'; statusHint.textContent = 'запись…';
|
||||
asrNote.classList.add('on'); asrText.textContent = '';
|
||||
let i = 0;
|
||||
recTimer = setInterval(() => {
|
||||
recSec++;
|
||||
document.getElementById('recTime').textContent = '0:' + String(recSec).padStart(2,'0');
|
||||
if (i < WORDS.length && recSec % 1 === 0) { // живая расшифровка
|
||||
asrText.textContent = WORDS.slice(0, ++i).join(' ') + (i < WORDS.length ? ' …' : '');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
function stopRec(cancel) {
|
||||
clearInterval(recTimer);
|
||||
recPanel.classList.remove('on'); recBtn.classList.remove('recording');
|
||||
inputRow.style.opacity = '1'; asrNote.classList.remove('on');
|
||||
if (cancel) { statusHint.textContent = 'запись отменена'; return; }
|
||||
if (recSec < 1) { statusHint.textContent = 'слишком коротко'; return; }
|
||||
ta.value = WORDS.join(' ') + '.'; // распознанный текст попадает в поле
|
||||
ta.dispatchEvent(new Event('input'));
|
||||
statusHint.textContent = 'распознано, ' + recSec + ' с — проверь и отправь';
|
||||
ta.focus();
|
||||
}
|
||||
recBtn.onclick = () => recPanel.classList.contains('on') ? stopRec(false) : startRec();
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); recBtn.click(); }
|
||||
});
|
||||
|
||||
// ---------- широкое / узкое окно ----------
|
||||
const win = document.querySelector('.window');
|
||||
const NARROW = 880;
|
||||
function applyMode() {
|
||||
if (innerWidth > NARROW) { // широкое: видно и то, и другое
|
||||
win.classList.remove('mode-chat', 'mode-list');
|
||||
return;
|
||||
}
|
||||
// узкое: показываем что-то одно, по умолчанию список
|
||||
if (!win.classList.contains('mode-chat') && !win.classList.contains('mode-list')) {
|
||||
win.classList.add('mode-list');
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.chat-row').forEach(r => r.addEventListener('click', () => {
|
||||
document.querySelectorAll('.chat-row').forEach(x => x.classList.remove('active'));
|
||||
r.classList.add('active');
|
||||
if (innerWidth <= NARROW) win.classList.remove('mode-list'), win.classList.add('mode-chat');
|
||||
}));
|
||||
document.getElementById('backBtn').onclick = () => win.classList.remove('mode-chat'), win.classList.add('mode-list');
|
||||
addEventListener('resize', applyMode);
|
||||
applyMode();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,404 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Одобрено: Агенты — список, форма, проверка связи</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2;
|
||||
--err: #EC3942; --ok: #63C88A;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ============ затемнение: чуть гасит окно под окном проверки ============ */
|
||||
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 200; display: none; }
|
||||
.scrim.on { display: block; }
|
||||
|
||||
/* ============ ОКНО 1: НАСТРОЙКИ ============ */
|
||||
.dialog {
|
||||
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7); position: relative; z-index: 150;
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* ── список агентов ── */
|
||||
.agent-card {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.agent-card:hover { border-color: var(--line); }
|
||||
.avatar {
|
||||
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-size: 13px; font-weight: 600; color: #0F1922; background: var(--accent); overflow: hidden;
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
/* значок-картинка: клиент показывает свою копию файла */
|
||||
.avatar.pic { background: transparent; padding: 0; }
|
||||
.avatar.pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
|
||||
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
|
||||
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
|
||||
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* ── форма ── */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
.note code { background: var(--panel2); padding: 1px 4px; border-radius: 4px; font-size: 10.5px; }
|
||||
.addr-row { display: flex; gap: 8px; }
|
||||
.addr-row input { flex: 1; }
|
||||
|
||||
/* ── цвет ИЛИ картинка ── */
|
||||
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
|
||||
.appearance-tabs .tab {
|
||||
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.pane { display: none; } .pane.on { display: block; }
|
||||
|
||||
.colors { display: flex; gap: 9px; }
|
||||
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
|
||||
.swatch.on { border-color: var(--fg); }
|
||||
|
||||
.pic-row { display: flex; align-items: center; gap: 13px; }
|
||||
.pic-preview {
|
||||
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
|
||||
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
|
||||
font-size: 20px; overflow: hidden;
|
||||
}
|
||||
.pic-preview.filled { border-style: solid; }
|
||||
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
/* ── кнопки ── */
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.prim:disabled { background: var(--panel2); border-color: var(--line); color: var(--dim); cursor: default; }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ============ ОКНО 2: ПРОВЕРКА СВЯЗИ (поверх настроек) ============ */
|
||||
.layer { position: fixed; inset: 0; display: none; align-items: center; justify-content: center;
|
||||
z-index: 250; pointer-events: none; }
|
||||
.layer.on { display: flex; }
|
||||
.layer > * { pointer-events: auto; }
|
||||
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
|
||||
|
||||
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
|
||||
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
|
||||
@keyframes sp { to { transform: rotate(360deg); } }
|
||||
|
||||
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
|
||||
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-size: 19px; flex: 0 0 40px; }
|
||||
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
|
||||
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
|
||||
.probe-head.busy .big { background: transparent; padding: 0; }
|
||||
.probe-head .txt { flex: 1; }
|
||||
.probe-head .ttl { font-size: 14px; font-weight: 600; }
|
||||
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px; }
|
||||
|
||||
.probe-list { font-size: 12px; }
|
||||
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.probe-list div:last-child { border-bottom: none; }
|
||||
.probe-list span { color: var(--dim); }
|
||||
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; }
|
||||
|
||||
.hint-line { font-size: 11.5px; color: var(--dim); margin-top: 12px; padding: 9px 11px;
|
||||
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ================= ОКНО 1: НАСТРОЙКИ ================= -->
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2 id="dTitle">Агенты</h2>
|
||||
<button class="x-btn">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- ── вид 1: список ── -->
|
||||
<div class="d-body" id="viewList">
|
||||
<div class="agent-card">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state"><span class="d"></span>на связи</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.88.135:8080/agentik',true)">⇄</button>
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-card">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state err"><span class="d"></span>не отвечает</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.76.199:8080/agentik',false)">⇄</button>
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-card">
|
||||
<div class="avatar pic"><img src="data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E" alt=""></div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">Кот-ассистент</div>
|
||||
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state off"><span class="d"></span>не проверялся</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.88.24:8080/agentik',true)">⇄</button>
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn wide" onclick="show('add')">+ Добавить агента</button>
|
||||
</div>
|
||||
|
||||
<!-- ── вид 2: форма нового ── -->
|
||||
<div class="d-body" id="viewAdd" style="display:none">
|
||||
<div class="field">
|
||||
<label>Название</label>
|
||||
<input type="text" value="рабочий" placeholder="как вы его будете звать">
|
||||
<div class="note">Как называть этого агента в списке. Придумываете сами — сервер своего имени не сообщает.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Адрес сервера агента</label>
|
||||
<div class="addr-row">
|
||||
<input type="text" class="mono" id="addrField" value="http://192.168.88.135:8080/agentik"
|
||||
placeholder="http://хост:порт/agentik">
|
||||
<button class="btn sm" onclick="openCheck('form', document.getElementById('addrField').value, true)">Проверить связь</button>
|
||||
</div>
|
||||
<div class="note">Адрес целиком, вместе с путём <code>/agentik</code>. Если агент
|
||||
на этой же машине — <code>http://localhost:8080/agentik</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Как отличать в списке</label>
|
||||
<div class="appearance-tabs">
|
||||
<div class="tab on" data-tab="color">Цвет</div>
|
||||
<div class="tab" data-tab="pic">Картинка</div>
|
||||
</div>
|
||||
|
||||
<div class="pane on" id="paneColor">
|
||||
<div class="colors">
|
||||
<div class="swatch on" style="background:#6AB2F2"></div>
|
||||
<div class="swatch" style="background:#86C99A"></div>
|
||||
<div class="swatch" style="background:#C9A886"></div>
|
||||
<div class="swatch" style="background:#B79AE0"></div>
|
||||
<div class="swatch" style="background:#E28C8C"></div>
|
||||
</div>
|
||||
<div class="note">Цветом помечаются диалоги агента в списке — полоска у строки
|
||||
и точка у группы.</div>
|
||||
</div>
|
||||
|
||||
<div class="pane" id="panePic">
|
||||
<div class="pic-row">
|
||||
<div class="pic-preview" id="picPreview">🖼</div>
|
||||
<div class="pic-actions">
|
||||
<button class="btn sm" onclick="pickPic()">Выбрать файл…</button>
|
||||
<button class="btn sm" id="picClear" onclick="clearPic()" style="display:none">Убрать</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">Картинка копируется <b>внутрь клиента</b> — по исходному адресу
|
||||
не подтягивается. Иначе значок пропадёт, если файл удалят, флешку вынут
|
||||
или папку переименуют.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot" id="footList">
|
||||
<button class="btn prim" style="flex:1">Готово</button>
|
||||
</div>
|
||||
<div class="d-foot" id="footAdd" style="display:none">
|
||||
<button class="btn" style="flex:1" onclick="show('list')">Отмена</button>
|
||||
<button class="btn prim" style="flex:1">Добавить агента</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scrim" id="scrim"></div>
|
||||
|
||||
<!-- ================= ОКНО 2: ПРОВЕРКА СВЯЗИ ================= -->
|
||||
<div class="layer" id="layerCheck">
|
||||
<div class="dialog small">
|
||||
<div class="d-head">
|
||||
<h2>Проверка связи</h2>
|
||||
<button class="x-btn" onclick="closeCheck()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="d-body">
|
||||
<div class="probe-head" id="probeHead">
|
||||
<div class="big" id="probeIcon"></div>
|
||||
<div class="txt">
|
||||
<div class="ttl" id="probeTitle">Спрашиваю агента…</div>
|
||||
<div class="sub" id="probeSub">http://192.168.88.135:8080/agentik</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="probe-list" id="probeList" style="display:none">
|
||||
<div><span>Ответ на запрос</span><b id="p1">—</b></div>
|
||||
<div><span>Отвечает по адресу</span><b id="p2">—</b></div>
|
||||
<div><span>Диалогов у агента</span><b id="p3">—</b></div>
|
||||
<div><span>Отклик</span><b id="p4">—</b></div>
|
||||
</div>
|
||||
|
||||
<div class="hint-line" id="probeHint" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1" id="probeAgain" onclick="runCheck()">Проверить снова</button>
|
||||
<button class="btn prim" style="flex:1" id="probeOk" disabled>Добавить агента</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function el(id){ return document.getElementById(id); }
|
||||
|
||||
// ── переключение список / форма ──
|
||||
function show(v) {
|
||||
const isList = (v === 'list');
|
||||
el('viewList').style.display = isList ? 'block' : 'none';
|
||||
el('viewAdd').style.display = isList ? 'none' : 'block';
|
||||
el('footList').style.display = isList ? 'flex' : 'none';
|
||||
el('footAdd').style.display = isList ? 'none' : 'flex';
|
||||
el('dTitle').textContent = isList ? 'Агенты' : 'Новый агент';
|
||||
}
|
||||
|
||||
// ── цвет ИЛИ картинка ──
|
||||
document.querySelectorAll('.appearance-tabs .tab').forEach(tab => tab.onclick = () => {
|
||||
document.querySelectorAll('.appearance-tabs .tab').forEach(x => x.classList.remove('on'));
|
||||
tab.classList.add('on');
|
||||
const pic = (tab.dataset.tab === 'pic');
|
||||
el('paneColor').classList.toggle('on', !pic);
|
||||
el('panePic').classList.toggle('on', pic);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.swatch').forEach(s => s.onclick = () => {
|
||||
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('on'));
|
||||
s.classList.add('on');
|
||||
});
|
||||
|
||||
// выбор картинки: в настоящем клиенте — системный диалог выбора файла,
|
||||
// а выбранный файл КОПИРУЕТСЯ в папку клиента (не ссылка на исходный путь)
|
||||
function pickPic() {
|
||||
setPic('data:image/svg+xml;utf8,' + encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54">
|
||||
<rect width="54" height="54" fill="#86C99A"/>
|
||||
<circle cx="27" cy="21" r="8" fill="#121218"/>
|
||||
<path d="M10 50 Q27 30 44 50 Z" fill="#121218"/>
|
||||
</svg>`));
|
||||
}
|
||||
function setPic(src) {
|
||||
const p = el('picPreview');
|
||||
p.innerHTML = `<img src="${src}" alt="">`;
|
||||
p.classList.add('filled');
|
||||
el('picClear').style.display = '';
|
||||
}
|
||||
function clearPic() {
|
||||
const p = el('picPreview');
|
||||
p.innerHTML = '🖼';
|
||||
p.classList.remove('filled');
|
||||
el('picClear').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── ОКНО ПРОВЕРКИ: открывается из формы или из списка ──
|
||||
let checkAddr = '', checkOk = true;
|
||||
|
||||
function openCheck(from, addr, ok) {
|
||||
checkAddr = addr; checkOk = ok;
|
||||
el('probeOk').textContent = (from === 'form') ? 'Добавить агента' : 'Готово';
|
||||
el('scrim').classList.add('on');
|
||||
el('layerCheck').classList.add('on');
|
||||
runCheck();
|
||||
}
|
||||
function closeCheck() {
|
||||
el('layerCheck').classList.remove('on');
|
||||
el('scrim').classList.remove('on');
|
||||
}
|
||||
|
||||
function runCheck() {
|
||||
const list = el('probeList'), hint = el('probeHint');
|
||||
el('probeHead').className = 'probe-head busy';
|
||||
el('probeIcon').outerHTML = '<span class="spin" id="probeIcon"></span>';
|
||||
el('probeTitle').textContent = 'Спрашиваю агента…';
|
||||
el('probeSub').textContent = checkAddr;
|
||||
list.style.display = 'none';
|
||||
hint.style.display = 'none';
|
||||
el('probeOk').disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('probeIcon').outerHTML = '<div class="big" id="probeIcon"></div>';
|
||||
el('probeSub').textContent = checkAddr;
|
||||
|
||||
if (!checkOk) {
|
||||
el('probeHead').className = 'probe-head err';
|
||||
el('probeIcon').textContent = '✕';
|
||||
el('probeTitle').textContent = 'Агент не отвечает';
|
||||
el('p1').textContent = 'нет ответа'; el('p2').textContent = '—';
|
||||
el('p3').textContent = '—'; el('p4').textContent = '5000 мс (истекло)';
|
||||
list.style.display = 'block'; hint.style.display = 'block';
|
||||
hint.textContent = 'Проверьте адрес и что агент запущен. Убедитесь, что порт открыт и путь /agentik указан целиком.';
|
||||
el('probeOk').disabled = true;
|
||||
} else {
|
||||
el('probeHead').className = 'probe-head ok';
|
||||
el('probeIcon').textContent = '✓';
|
||||
el('probeTitle').textContent = 'Агент отвечает';
|
||||
el('p1').textContent = 'ok'; el('p2').textContent = checkAddr;
|
||||
el('p3').textContent = '4'; el('p4').textContent = '38 мс';
|
||||
list.style.display = 'block'; hint.style.display = 'block';
|
||||
hint.textContent = 'Адрес рабочий. Можно добавлять этого агента.';
|
||||
el('probeOk').disabled = false;
|
||||
}
|
||||
}, 1400);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,144 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Одобрено: Новый диалог — выбор ассистента</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --err: #EC3942; --ok: #63C88A;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 480px; min-width: 480px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
|
||||
|
||||
.agent-opt {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; cursor: pointer; border: 1.5px solid transparent;
|
||||
}
|
||||
.agent-opt:hover { border-color: #33414F; }
|
||||
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
|
||||
.avatar {
|
||||
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-size: 13px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
/* аватар-картинка: клиент показывает свою копию файла */
|
||||
.avatar.pic {
|
||||
background: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E") center/cover;
|
||||
}
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
|
||||
.ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.ag-state.ok { color: var(--ok); }
|
||||
.ag-state.off { color: var(--dim); } .ag-state.off .d { background: var(--dim); }
|
||||
.ag-state.err { color: var(--err); } .ag-state.err .d { background: var(--err); }
|
||||
|
||||
.pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%; border: 2px solid #3D4A58; }
|
||||
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
|
||||
.agent-opt.on .pick::after {
|
||||
content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 9px;
|
||||
border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg);
|
||||
}
|
||||
|
||||
.warn {
|
||||
display: none; gap: 8px; align-items: flex-start; margin-top: 4px; padding: 10px 12px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA;
|
||||
}
|
||||
.warn.on { display: flex; }
|
||||
|
||||
.btn {
|
||||
padding: 10px 16px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel2);
|
||||
color: var(--fg); font-size: 13.5px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.btn:hover { border-color: #3D4A58; }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Новый диалог</h2>
|
||||
<button class="x-btn">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="d-body">
|
||||
<div class="d-lead">В каком ассистенте создать диалог?</div>
|
||||
|
||||
<div class="agent-opt on" data-ok="1">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="agent-opt" data-ok="1">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="agent-opt" data-ok="0">
|
||||
<div class="avatar pic"></div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">Кот-ассистент</div>
|
||||
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
|
||||
<div class="ag-state err"><span class="d"></span>не отвечает</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="warn" id="warn">
|
||||
<span>⚠</span>
|
||||
<span>Ассистент сейчас не отвечает. Диалог создать можно, но
|
||||
сообщение не уйдёт, пока он не поднимется.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1">Отмена</button>
|
||||
<button class="btn prim" style="flex:1">Создать диалог</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Выбор ассистента. Первый отмечен сразу — создать диалог можно одним нажатием.
|
||||
document.querySelectorAll('.agent-opt').forEach(o => o.onclick = () => {
|
||||
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
|
||||
o.classList.add('on');
|
||||
document.getElementById('warn').classList.toggle('on', o.dataset.ok === '0');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,296 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Группы — список</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
|
||||
/* ============ окно клиента на фоне ============ */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
|
||||
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* ── лента групп ── */
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
|
||||
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
|
||||
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
|
||||
.folder.act:hover { color: var(--fg); background: var(--panel2); }
|
||||
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
.avatar.g4 { background: #B79AE0; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
|
||||
border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
|
||||
align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ затемнение и модалка ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
|
||||
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
|
||||
|
||||
.dialog {
|
||||
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* ── строка группы ── */
|
||||
.grp {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.grp:hover { border-color: var(--line); }
|
||||
.grp .mid { flex: 1; min-width: 0; }
|
||||
.grp .nm { font-size: 14px; font-weight: 550; }
|
||||
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* служебная группа — «Все чаты»: не правится и не удаляется */
|
||||
.grp.sys { background: transparent; border: 1px dashed var(--line); }
|
||||
.grp.sys .nm { color: var(--fg); }
|
||||
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
|
||||
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ── форма ── */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
|
||||
/* ── предупреждение ── */
|
||||
.warn {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA; line-height: 1.5;
|
||||
}
|
||||
.warn .i { flex: 0 0 auto; font-size: 13px; }
|
||||
.info {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
|
||||
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
|
||||
font-size: 11.5px; color: var(--dim); line-height: 1.5;
|
||||
}
|
||||
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
|
||||
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
|
||||
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="window">
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="icon-btn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">Учёба</div>
|
||||
<div class="folder">Личное</div>
|
||||
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list">
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g4">ПО</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Девайт-нет стартовал</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<div class="avatar">АГ</div>
|
||||
<div>
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="messages">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="scrim"></div>
|
||||
<div class="layer">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Группы</h2>
|
||||
<button class="x-btn">✕</button>
|
||||
</div>
|
||||
<div class="d-body">
|
||||
<div class="grp sys">
|
||||
<div class="mid">
|
||||
<div class="nm">Все чаты</div>
|
||||
<div class="sub">Показывает все диалоги, из любой группы</div>
|
||||
</div>
|
||||
<div class="lock">встроенная</div>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="mid">
|
||||
<div class="nm">Работа</div>
|
||||
<div class="sub">12 диалогов</div>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="mid">
|
||||
<div class="nm">Дом</div>
|
||||
<div class="sub">4 диалога</div>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="mid">
|
||||
<div class="nm">Учёба</div>
|
||||
<div class="sub">2 диалога</div>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="mid">
|
||||
<div class="nm">Личное</div>
|
||||
<div class="sub">пустая</div>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn wide">+ Добавить группу</button>
|
||||
</div>
|
||||
<div class="d-foot">
|
||||
<button class="btn prim" style="flex:1">Готово</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,255 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Группы — новая</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
|
||||
/* ============ окно клиента на фоне ============ */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
|
||||
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* ── лента групп ── */
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
|
||||
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
|
||||
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
|
||||
.folder.act:hover { color: var(--fg); background: var(--panel2); }
|
||||
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
.avatar.g4 { background: #B79AE0; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
|
||||
border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
|
||||
align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ затемнение и модалка ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
|
||||
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
|
||||
|
||||
.dialog {
|
||||
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* ── строка группы ── */
|
||||
.grp {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.grp:hover { border-color: var(--line); }
|
||||
.grp .mid { flex: 1; min-width: 0; }
|
||||
.grp .nm { font-size: 14px; font-weight: 550; }
|
||||
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* служебная группа — «Все чаты»: не правится и не удаляется */
|
||||
.grp.sys { background: transparent; border: 1px dashed var(--line); }
|
||||
.grp.sys .nm { color: var(--fg); }
|
||||
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
|
||||
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ── форма ── */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
|
||||
/* ── предупреждение ── */
|
||||
.warn {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA; line-height: 1.5;
|
||||
}
|
||||
.warn .i { flex: 0 0 auto; font-size: 13px; }
|
||||
.info {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
|
||||
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
|
||||
font-size: 11.5px; color: var(--dim); line-height: 1.5;
|
||||
}
|
||||
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
|
||||
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
|
||||
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="window">
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="icon-btn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">Учёба</div>
|
||||
<div class="folder">Личное</div>
|
||||
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list">
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g4">ПО</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Девайт-нет стартовал</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<div class="avatar">АГ</div>
|
||||
<div>
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="messages">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="scrim"></div>
|
||||
<div class="layer">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Новая группа</h2>
|
||||
<button class="x-btn">✕</button>
|
||||
</div>
|
||||
<div class="d-body">
|
||||
<div class="field">
|
||||
<label>Название</label>
|
||||
<input type="text" value="Учёба" placeholder="как назвать группу">
|
||||
<div class="note">Так группа будет называться в ленте над списком диалогов.</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<span>ℹ</span>
|
||||
<span>Диалоги в группу попадают потом — из самого диалога. Здесь группа
|
||||
только создаётся, диалог в неё не переносится.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1">Отмена</button>
|
||||
<button class="btn prim" style="flex:1">Создать группу</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,260 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Группы — изменение</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
|
||||
/* ============ окно клиента на фоне ============ */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
|
||||
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* ── лента групп ── */
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
|
||||
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
|
||||
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
|
||||
.folder.act:hover { color: var(--fg); background: var(--panel2); }
|
||||
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
.avatar.g4 { background: #B79AE0; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
|
||||
border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
|
||||
align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ затемнение и модалка ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
|
||||
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
|
||||
|
||||
.dialog {
|
||||
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* ── строка группы ── */
|
||||
.grp {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.grp:hover { border-color: var(--line); }
|
||||
.grp .mid { flex: 1; min-width: 0; }
|
||||
.grp .nm { font-size: 14px; font-weight: 550; }
|
||||
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* служебная группа — «Все чаты»: не правится и не удаляется */
|
||||
.grp.sys { background: transparent; border: 1px dashed var(--line); }
|
||||
.grp.sys .nm { color: var(--fg); }
|
||||
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
|
||||
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ── форма ── */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
|
||||
/* ── предупреждение ── */
|
||||
.warn {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA; line-height: 1.5;
|
||||
}
|
||||
.warn .i { flex: 0 0 auto; font-size: 13px; }
|
||||
.info {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
|
||||
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
|
||||
font-size: 11.5px; color: var(--dim); line-height: 1.5;
|
||||
}
|
||||
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
|
||||
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
|
||||
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="window">
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="icon-btn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder active">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">Учёба</div>
|
||||
<div class="folder">Личное</div>
|
||||
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list">
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g4">ПО</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Девайт-нет стартовал</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<div class="avatar">АГ</div>
|
||||
<div>
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="messages">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="scrim"></div>
|
||||
<div class="layer">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Группа «Работа»</h2>
|
||||
<button class="x-btn">✕</button>
|
||||
</div>
|
||||
<div class="d-body">
|
||||
<div class="field">
|
||||
<label>Название</label>
|
||||
<input type="text" value="Работа" placeholder="как назвать группу">
|
||||
<div class="note">Так группа будет называться в ленте над списком диалогов.</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<span>ℹ</span>
|
||||
<span>В группе сейчас 12 диалогов. Переименование их не затрагивает —
|
||||
это только название группы.</span>
|
||||
</div>
|
||||
|
||||
<div class="danger-zone">
|
||||
<div class="lbl">Опасное</div>
|
||||
<button class="btn dang" style="width:100%">Удалить группу</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1">Отмена</button>
|
||||
<button class="btn prim" style="flex:1">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,267 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Группы — удаление</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
|
||||
/* ============ окно клиента на фоне ============ */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px; }
|
||||
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* ── лента групп ── */
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
/* кнопка «Группы» — не папка, а действие: справа, отдельно от чипов */
|
||||
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
|
||||
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
|
||||
.folder.act:hover { color: var(--fg); background: var(--panel2); }
|
||||
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer; align-items: flex-start; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
.avatar.g4 { background: #B79AE0; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
|
||||
border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex;
|
||||
align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ затемнение и модалка ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; }
|
||||
.layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 150; }
|
||||
|
||||
.dialog {
|
||||
width: 520px; min-width: 520px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* ── строка группы ── */
|
||||
.grp {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.grp:hover { border-color: var(--line); }
|
||||
.grp .mid { flex: 1; min-width: 0; }
|
||||
.grp .nm { font-size: 14px; font-weight: 550; }
|
||||
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* служебная группа — «Все чаты»: не правится и не удаляется */
|
||||
.grp.sys { background: transparent; border: 1px dashed var(--line); }
|
||||
.grp.sys .nm { color: var(--fg); }
|
||||
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
|
||||
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ── форма ── */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
|
||||
/* ── предупреждение ── */
|
||||
.warn {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA; line-height: 1.5;
|
||||
}
|
||||
.warn .i { flex: 0 0 auto; font-size: 13px; }
|
||||
.info {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
|
||||
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
|
||||
font-size: 11.5px; color: var(--dim); line-height: 1.5;
|
||||
}
|
||||
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
|
||||
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
|
||||
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.82); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="window">
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="icon-btn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder active">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">Учёба</div>
|
||||
<div class="folder">Личное</div>
|
||||
<div class="folder act" title="Группы: добавить, изменить, удалить">✎</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list">
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g4">ПО</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Девайт-нет стартовал</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<div class="avatar">АГ</div>
|
||||
<div>
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="messages">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="scrim"></div>
|
||||
<div class="layer">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Удалить группу?</h2>
|
||||
<button class="x-btn">✕</button>
|
||||
</div>
|
||||
<div class="d-body">
|
||||
<div class="warn">
|
||||
<span class="i">⚠</span>
|
||||
<span>Группа <b>«Работа»</b> исчезнет из ленты. <b>Диалоги не удалятся</b> —
|
||||
они останутся и будут видны в «Все чаты». Вернуть группу можно, создав её
|
||||
заново, но диалоги придётся разложить руками.</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Что сделать с диалогами группы</label>
|
||||
<div class="grp" style="margin-bottom:9px">
|
||||
<div class="mid">
|
||||
<div class="nm">Оставить без группы</div>
|
||||
<div class="sub">12 диалогов вернутся в «Все чаты»</div>
|
||||
</div>
|
||||
<div class="lock">✓</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<span>ℹ</span>
|
||||
<span>Другого пути нет: диалог обязан быть где-то виден. Отдельной
|
||||
«корзины» у диалогов нет — удаляются только диалоги, и не отсюда.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1">Отмена</button>
|
||||
<button class="btn dang solid" style="flex:1">Удалить группу</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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)
|
||||
|
||||
**Главное, что говорит окно: диалоги не удалятся.** Группа исчезнет из ленты,
|
||||
диалоги останутся и будут видны в «Все чаты». Это первое, что написано
|
||||
в предупреждении, — потому что именно этого боятся, нажимая «удалить».
|
||||
|
||||
Отдельный вопрос — **что сделать с диалогами**: оставить без группы (вернутся
|
||||
в «Все чаты»). Другого пути нет: диалог обязан быть где-то виден, «корзины»
|
||||
у диалогов не существует. Так и написано в окне — чтобы не выглядело, будто
|
||||
выбор есть, а его нет.
|
||||
|
||||
Кнопка подтверждения — **красная залитая**, в тексте прямо «Удалить группу».
|
||||
|
||||
---
|
||||
|
||||
## Что решено и почему
|
||||
|
||||
- **Карандаш вместо «+».** Иначе две кнопки подряд означают «создать» — и в шапке
|
||||
списка, и в ленте групп.
|
||||
- **«Все чаты» не удаляется.** Нужна точка опоры, где собран весь список.
|
||||
- **Диалоги не удаляются вместе с группой.** Иначе одно нажатие стирает работу.
|
||||
- **Группа без диалогов допустима.** Создать заранее — нормальный сценарий.
|
||||
- **Никаких настроек у группы, кроме названия.** Цвет, значок, порядок — не
|
||||
просили, не делаем.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- **Куда диалог кладётся при создании** — в текущую открытую группу или без
|
||||
группы? Сейчас в макете не показано.
|
||||
- **Можно ли диалог быть в нескольких группах** (как ярлык в почте) или только
|
||||
в одной? От этого зависит, что писать в удалении.
|
||||
- **Порядок групп** — по алфавиту, по времени создания или перетаскиванием?
|
||||
- **Что если удалить группу, в которой открыт диалог.** Сейчас не показано.
|
||||
@@ -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` — четыре файла, открывать по одному, по состоянию.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Вариант 2 — Rail / Voice-first
|
||||
|
||||
## Суть
|
||||
Голос — главное действие, а не дополнение к клавиатуре. Огромная кнопка микрофона
|
||||
слева в панели ввода, живая расшифровка на экране, пока говоришь.
|
||||
|
||||
## Ключевые решения
|
||||
- **Раскладка:** рейка иконок (76) + диалоги (296) + чат. Рейка — вход в память агента
|
||||
и инструменты, это не просто мессенджер.
|
||||
- **Голос:** кнопка 58 px, при записи краснеет и пульсирует. Над полем раскрывается
|
||||
панель: волна, таймер и **текст, который появляется по мере речи**.
|
||||
- **Три выхода из записи:** «Отправить» / «В поле ввода» (поправить руками) / «Отменить».
|
||||
Голосовое сообщение в ленте рисуется с волной и кнопкой проигрывания.
|
||||
- **Горячие клавиши:** Ctrl+R — запись, удержание пробела — говорить, Esc — отмена.
|
||||
|
||||
## Что хорошо
|
||||
- Голосом пользоваться реально удобно: видно, что тебя услышали, до отправки.
|
||||
- Ошибку в тексте видно до отправки — можно поправить.
|
||||
|
||||
## Что слабо
|
||||
- Больше элементов на экране, надо разбираться.
|
||||
- «Пробел на удержание» конфликтует с обычным набором текста — нужна аккуратность.
|
||||
|
||||
## Кому
|
||||
Тому, кто в основном диктует, а не печатает.
|
||||
@@ -1,342 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Вариант 2 — Rail / Voice-first</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0E1418; --panel: #141E24; --panel2: #1B2830; --line: #22323C;
|
||||
--fg: #E8EEF2; --dim: #7A93A3; --accent: #4FD1C5; --hot: #FF6B6B;
|
||||
--bot-bubble: #1B2830; --user-bubble: #1F4B54;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #070B0D; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
.window {
|
||||
width: 1240px; height: 750px; background: var(--bg); border-radius: 14px; display: flex;
|
||||
overflow: hidden; border: 1px solid var(--line); box-shadow: 0 24px 80px rgba(0,0,0,.65);
|
||||
}
|
||||
|
||||
/* ---- узкая колонка-рейка: чаты иконками ---- */
|
||||
.rail {
|
||||
width: 76px; flex: 0 0 76px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; align-items: center; padding: 12px 0; gap: 10px;
|
||||
}
|
||||
.rail-btn {
|
||||
width: 46px; height: 46px; border-radius: 13px; border: none; background: var(--panel2);
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px;
|
||||
position: relative; transition: .15s;
|
||||
}
|
||||
.rail-btn:hover { color: var(--fg); }
|
||||
.rail-btn.active { background: var(--accent); color: #07201D; }
|
||||
.rail-btn .dot-badge {
|
||||
position: absolute; top: -3px; right: -3px; background: var(--accent); color: #07201D;
|
||||
font-size: 10px; font-weight: 800; border-radius: 8px; padding: 1px 5px; min-width: 16px;
|
||||
}
|
||||
.rail-btn .label {
|
||||
position: absolute; bottom: -14px; font-size: 9px; color: var(--dim); white-space: nowrap;
|
||||
}
|
||||
.rail-sep { width: 28px; height: 1px; background: var(--line); margin: 8px 0; }
|
||||
.rail-spacer { flex: 1; }
|
||||
|
||||
/* ---- средний столбец: список диалогов ---- */
|
||||
.list { width: 296px; flex: 0 0 296px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; }
|
||||
.l-head { padding: 16px 18px 12px; }
|
||||
.l-head h1 { font-size: 16.5px; font-weight: 650; letter-spacing: .2px; }
|
||||
.l-head p { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.search {
|
||||
margin: 0 18px 10px; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13px; outline: none;
|
||||
}
|
||||
.search:focus { border-color: var(--accent); }
|
||||
.search::placeholder { color: var(--dim); }
|
||||
.rows { flex: 1; overflow-y: auto; padding: 0 10px 12px; }
|
||||
.row {
|
||||
display: flex; gap: 11px; padding: 10px 11px; border-radius: 11px; cursor: pointer;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
.row:hover { background: var(--panel2); }
|
||||
.row.active { background: var(--panel2); border-left-color: var(--accent); }
|
||||
.row .av {
|
||||
width: 38px; height: 38px; flex: 0 0 38px; border-radius: 11px; display: grid; place-items: center;
|
||||
font-size: 13px; font-weight: 700; background: rgba(79,209,197,.16); color: var(--accent);
|
||||
}
|
||||
.row .mid { flex: 1; min-width: 0; }
|
||||
.row .nm { font-size: 13.5px; font-weight: 550; display: flex; justify-content: space-between; }
|
||||
.row .tm { font-size: 10.5px; color: var(--dim); font-weight: 400; }
|
||||
.row .pv { font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
|
||||
/* ---- основная зона чата ---- */
|
||||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.m-head { height: 60px; flex: 0 0 60px; display: flex; align-items: center; gap: 12px;
|
||||
padding: 0 22px; border-bottom: 1px solid var(--line); }
|
||||
.m-head .t { font-size: 14.5px; font-weight: 600; }
|
||||
.m-head .s { font-size: 11.5px; color: var(--dim); }
|
||||
.ghost { margin-left: auto; display: flex; gap: 6px; }
|
||||
.ghost button { background: transparent; border: 1px solid var(--line); color: var(--dim);
|
||||
border-radius: 8px; padding: 5px 11px; font-size: 11.5px; cursor: pointer; }
|
||||
.ghost button:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
.thread { flex: 1; overflow-y: auto; padding: 22px 26px; display: flex; flex-direction: column; gap: 13px; }
|
||||
.b { max-width: 76%; padding: 10px 14px; border-radius: 14px; font-size: 13.5px; }
|
||||
.b.u { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.b.a { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
.b .tm { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
|
||||
|
||||
/* голосовое сообщение — с волной прямо в пузыре */
|
||||
.voice-b {
|
||||
align-self: flex-end; background: var(--user-bubble); border-radius: 14px 14px 4px 14px;
|
||||
padding: 10px 14px; display: flex; align-items: center; gap: 11px; min-width: 250px;
|
||||
}
|
||||
.play { width: 30px; height: 30px; border-radius: 50%; background: var(--accent); color: #07201D;
|
||||
border: none; display: grid; place-items: center; cursor: pointer; font-size: 12px; }
|
||||
.vwave { flex: 1; display: flex; align-items: center; gap: 2px; height: 24px; }
|
||||
.vwave i { flex: 1; background: rgba(232,238,242,.5); border-radius: 2px; }
|
||||
.vdur { font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---- КОМПОЗЕР: голос — главный ---- */
|
||||
.composer { border-top: 1px solid var(--line); background: var(--panel); padding: 16px 22px 14px; }
|
||||
|
||||
.mic-zone { display: flex; align-items: center; gap: 16px; }
|
||||
.mic-big {
|
||||
width: 58px; height: 58px; flex: 0 0 58px; border-radius: 50%; border: none; cursor: pointer;
|
||||
background: var(--accent); color: #07201D; font-size: 23px; display: grid; place-items: center;
|
||||
transition: .2s;
|
||||
}
|
||||
.mic-big:hover { transform: scale(1.05); }
|
||||
.mic-big.on { background: var(--hot); color: #fff; animation: ring 1.5s infinite; }
|
||||
@keyframes ring { 0%,100% { box-shadow: 0 0 0 0 rgba(255,107,107,.6); } 50% { box-shadow: 0 0 0 13px rgba(255,107,107,0); } }
|
||||
|
||||
.mic-side { flex: 1; min-width: 0; }
|
||||
.mic-hint { font-size: 12px; color: var(--dim); margin-bottom: 7px; }
|
||||
.mic-hint b { color: var(--fg); font-weight: 550; }
|
||||
.type-row { display: flex; align-items: center; gap: 9px; }
|
||||
.type-row textarea {
|
||||
flex: 1; background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 10px 13px;
|
||||
color: var(--fg); font-family: inherit; font-size: 13.5px; outline: none; resize: none;
|
||||
height: 40px; max-height: 110px; line-height: 1.4;
|
||||
}
|
||||
.type-row textarea:focus { border-color: var(--accent); }
|
||||
.type-row textarea::placeholder { color: var(--dim); }
|
||||
.go { height: 40px; padding: 0 17px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--dim); font-size: 12.5px; cursor: pointer; }
|
||||
.go.active { background: var(--accent); border-color: var(--accent); color: #07201D; font-weight: 650; }
|
||||
|
||||
/* расшифровка в реальном времени */
|
||||
.live-asr { margin-top: 11px; display: none; background: var(--bg); border: 1px solid var(--hot);
|
||||
border-radius: 11px; padding: 11px 14px; }
|
||||
.live-asr.on { display: block; }
|
||||
.live-asr .top { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; font-size: 11.5px; color: var(--hot); font-weight: 600; }
|
||||
.blink { width: 7px; height: 7px; border-radius: 50%; background: var(--hot); animation: bl .9s infinite; }
|
||||
@keyframes bl { 50% { opacity: .15; } }
|
||||
.big-wave { display: flex; align-items: center; gap: 3px; height: 30px; margin-bottom: 8px; }
|
||||
.big-wave i { width: 3px; background: var(--accent); border-radius: 2px; height: 25%; animation: bw .8s infinite ease-in-out; }
|
||||
.big-wave i:nth-child(2n) { animation-delay: .1s; } .big-wave i:nth-child(3n) { animation-delay: .2s; }
|
||||
@keyframes bw { 0%,100% { height: 18%; } 50% { height: 100%; } }
|
||||
.asr-line { font-size: 14px; color: var(--fg); min-height: 20px; }
|
||||
.asr-line .cursor { display: inline-block; width: 2px; height: 15px; background: var(--accent);
|
||||
vertical-align: middle; animation: bl .8s infinite; }
|
||||
.live-actions { display: flex; gap: 8px; margin-top: 11px; }
|
||||
.live-actions button { border-radius: 9px; padding: 7px 14px; font-size: 12.5px; cursor: pointer; border: 1px solid var(--line); background: transparent; color: var(--dim); }
|
||||
.live-actions .prim { background: var(--accent); border-color: var(--accent); color: #07201D; font-weight: 620; }
|
||||
.live-actions .dang { color: var(--hot); }
|
||||
.foot-note { font-size: 10.5px; color: var(--dim); margin-top: 9px; }
|
||||
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
|
||||
|
||||
/* окно целиком влезает на любой экран — не режем картинку */
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
|
||||
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
|
||||
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window">
|
||||
<!-- ============ РЕЙКА ============ -->
|
||||
<nav class="rail">
|
||||
<button class="rail-btn active" title="Диалоги">💬<span class="dot-badge">3</span></button>
|
||||
<button class="rail-btn" title="Память агента">🧠</button>
|
||||
<button class="rail-btn" title="Инструменты">⚙</button>
|
||||
<div class="rail-sep"></div>
|
||||
<button class="rail-btn" title="Новый диалог">+</button>
|
||||
<div class="rail-spacer"></div>
|
||||
<button class="rail-btn" title="Настройки">⚙︎</button>
|
||||
</nav>
|
||||
|
||||
<!-- ============ СПИСОК ДИАЛОГОВ ============ -->
|
||||
<section class="list">
|
||||
<div class="l-head">
|
||||
<h1>Диалоги</h1>
|
||||
<p>локальный агент · 3 непрочитанных</p>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск…">
|
||||
<div class="rows">
|
||||
<div class="row active">
|
||||
<div class="av">АГ</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>agentik</span><span class="tm">14:22</span></div>
|
||||
<div class="pv">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">ОЧ</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Отчёт за квартал</span><span class="tm">12:05</span></div>
|
||||
<div class="pv">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">РЗ</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Разбор логов</span><span class="tm">вчера</span></div>
|
||||
<div class="pv">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">ПО</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Solana-бэкенд</span><span class="tm">вчера</span></div>
|
||||
<div class="pv">Девайс-нет стартовал</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">КН</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Книги</span><span class="tm">пн</span></div>
|
||||
<div class="pv">Разложил по полкам</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ ЧАТ ============ -->
|
||||
<main class="main">
|
||||
<header class="m-head">
|
||||
<div>
|
||||
<div class="t">agentik</div>
|
||||
<div class="s">на связи · codding-big</div>
|
||||
</div>
|
||||
<div class="ghost">
|
||||
<button>Стоп</button>
|
||||
<button>Очистить</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="thread" id="thread">
|
||||
<div class="b u">Посмотри логи за вчера<span class="tm">14:10</span></div>
|
||||
<div class="b a">Проверил: 3 ошибки 502 около 04:20, все — обрыв соединения с upstream. Виновник — таймаут прокси. Поправить?<span class="tm">14:12</span></div>
|
||||
|
||||
<div class="voice-b">
|
||||
<button class="play" onclick="togglePlay(this)">▶</button>
|
||||
<div class="vwave" id="pastWave"></div>
|
||||
<span class="vdur">0:07</span>
|
||||
</div>
|
||||
|
||||
<div class="b a">Понял: увеличить таймаут до 60 секунд и перезапустить прокси. Сделал — прокси поднят заново, ошибок за последний час нет.<span class="tm">14:20</span></div>
|
||||
</div>
|
||||
|
||||
<div class="composer">
|
||||
<div class="live-asr" id="liveAsr">
|
||||
<div class="top"><span class="blink"></span> ЗАПИСЬ · <span id="liveTime">0:00</span></div>
|
||||
<div class="big-wave" id="bigWave"></div>
|
||||
<div class="asr-line"><span id="asrLine"></span><span class="cursor"></span></div>
|
||||
<div class="live-actions">
|
||||
<button class="prim" onclick="stopRec(false)">Отправить</button>
|
||||
<button onclick="insertAsText()">В поле ввода</button>
|
||||
<button class="dang" onclick="stopRec(true)">Отменить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mic-zone">
|
||||
<button class="mic-big" id="micBtn" title="Запись (Ctrl+R, удержание пробела)">🎙</button>
|
||||
<div class="mic-side">
|
||||
<div class="mic-hint"><b>Говорите</b> — текст появится в поле ниже. Можно править перед отправкой.</div>
|
||||
<div class="type-row">
|
||||
<textarea id="ta" placeholder="Или напишите руками…"></textarea>
|
||||
<button class="go" id="goBtn" disabled>Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot-note">
|
||||
<kbd>Ctrl</kbd>+<kbd>R</kbd> — начать/стоп · <kbd>Пробел</kbd> (удержание) — говорить · <kbd>Enter</kbd> — отправить · <kbd>Esc</kbd> — отменить запись
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ta = document.getElementById('ta'), goBtn = document.getElementById('goBtn');
|
||||
const micBtn = document.getElementById('micBtn'), liveAsr = document.getElementById('liveAsr');
|
||||
const thread = document.getElementById('thread');
|
||||
|
||||
document.getElementById('pastWave').innerHTML =
|
||||
Array.from({length: 34}, () => '<i style="height:' + (18 + Math.random()*80) + '%"></i>').join('');
|
||||
document.getElementById('bigWave').innerHTML = Array.from({length: 40}, () => '<i></i>').join('');
|
||||
|
||||
ta.addEventListener('input', () => {
|
||||
goBtn.disabled = ta.value.trim() === '';
|
||||
goBtn.classList.toggle('active', ta.value.trim() !== '');
|
||||
ta.style.height = '40px'; ta.style.height = Math.min(ta.scrollHeight, 110) + 'px';
|
||||
});
|
||||
|
||||
function send() {
|
||||
const t = ta.value.trim(); if (!t) return;
|
||||
add(t, 'u'); ta.value = ''; ta.style.height = '40px';
|
||||
goBtn.disabled = true; goBtn.classList.remove('active');
|
||||
setTimeout(() => add('Выполняю: «' + t + '».', 'a'), 800);
|
||||
}
|
||||
function add(text, who) {
|
||||
const d = document.createElement('div'); d.className = 'b ' + who;
|
||||
const n = new Date();
|
||||
d.innerHTML = text + '<span class="tm">' + n.getHours() + ':' + String(n.getMinutes()).padStart(2,'0') + '</span>';
|
||||
thread.appendChild(d); thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
goBtn.onclick = send;
|
||||
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
|
||||
|
||||
// ---- запись с живой расшифровкой ----
|
||||
const PHRASE = 'посмотри логи за вчерашний день и скажи что там с ошибками';
|
||||
let tmr = null, sec = 0, wordIdx = 0;
|
||||
function startRec() {
|
||||
sec = 0; wordIdx = 0;
|
||||
liveAsr.classList.add('on'); micBtn.classList.add('on');
|
||||
document.getElementById('asrLine').textContent = '';
|
||||
tmr = setInterval(() => {
|
||||
sec++;
|
||||
document.getElementById('liveTime').textContent = '0:' + String(sec).padStart(2,'0');
|
||||
const words = PHRASE.split(' ');
|
||||
if (wordIdx < words.length) {
|
||||
document.getElementById('asrLine').textContent = words.slice(0, ++wordIdx).join(' ');
|
||||
}
|
||||
}, 450);
|
||||
}
|
||||
function stopRec(cancel) {
|
||||
clearInterval(tmr);
|
||||
liveAsr.classList.remove('on'); micBtn.classList.remove('on');
|
||||
if (cancel) return;
|
||||
const text = document.getElementById('asrLine').textContent;
|
||||
if (!text) return;
|
||||
if (sec >= 2) { // короткие — сразу в чат, длинные — в поле
|
||||
add(text, 'u');
|
||||
setTimeout(() => add('Записал голосом, разбираю…', 'a'), 700);
|
||||
}
|
||||
}
|
||||
function insertAsText() {
|
||||
ta.value = (ta.value ? ta.value + ' ' : '') + document.getElementById('asrLine').textContent;
|
||||
ta.dispatchEvent(new Event('input')); ta.focus(); stopRec(true);
|
||||
}
|
||||
function togglePlay(btn) { btn.textContent = btn.textContent === '▶' ? '❚❚' : '▶'; }
|
||||
micBtn.onclick = () => liveAsr.classList.contains('on') ? stopRec(false) : startRec();
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); micBtn.click(); }
|
||||
if (e.key === 'Escape' && liveAsr.classList.contains('on')) stopRec(true);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,25 +0,0 @@
|
||||
# Вариант 3 — Three-pane / Command center
|
||||
|
||||
## Суть
|
||||
Три панели: диалоги, чат и **состояние агента**. Работа агента видна не как текст
|
||||
в переписке, а как отдельные раскрывающиеся шаги.
|
||||
|
||||
## Ключевые решения
|
||||
- **Раскладка:** 268 + центр + 306. Третья панель — модель, контекст, инструменты,
|
||||
память агента, файлы сессии.
|
||||
- **Шаг агента** — сворачиваемый блок: что делал, сколько заняло, детали по клику.
|
||||
Это то, чем агент отличается от собеседника, и здесь оно вынесено в структуру.
|
||||
- **Кнопка записи:** компактная рядом с «Отправить»; при записи снизу появляется
|
||||
строка: квадрат «стоп», индикатор уровня, текущая расшифровка, таймер.
|
||||
- **Короткая диктовка** (от 2 с) уходит сразу, текст остаётся в поле.
|
||||
|
||||
## Что хорошо
|
||||
- Видно, что агент делает прямо сейчас и сколько это стоит.
|
||||
- Удобно на широком экране, ничего не надо прятать.
|
||||
|
||||
## Что слабо
|
||||
- На узком экране (ноутбук 1366) третью панель придётся убирать.
|
||||
- Настройки сессии — не то, на что хочется смотреть постоянно.
|
||||
|
||||
## Кому
|
||||
Тому, кто держит агента включённым весь день и следит за расходом.
|
||||
@@ -1,345 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Вариант 3 — Three-pane / Command center</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0F1115; --panel: #161A21; --panel2: #1E242D; --panel3: #252C37; --line: #2A323D;
|
||||
--fg: #ECEFF3; --dim: #808FA0; --accent: #7C8CF8; --ok: #4ADE80; --warn: #FBBF24; --hot: #F87171;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #06080A; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 22px;
|
||||
}
|
||||
.win { width: 1360px; height: 780px; background: var(--bg); border-radius: 13px; overflow: hidden;
|
||||
border: 1px solid var(--line); display: flex; flex-direction: column; box-shadow: 0 26px 90px rgba(0,0,0,.7); }
|
||||
|
||||
/* ---- строка меню-заголовка ---- */
|
||||
.titlebar { height: 42px; flex: 0 0 42px; display: flex; align-items: center; gap: 12px;
|
||||
padding: 0 16px; background: var(--panel); border-bottom: 1px solid var(--line); }
|
||||
.lights { display: flex; gap: 7px; }
|
||||
.lights i { width: 11px; height: 11px; border-radius: 50%; }
|
||||
.lights i:nth-child(1) { background: #FF5F57; } .lights i:nth-child(2) { background: #FEBC2E; }
|
||||
.lights i:nth-child(3) { background: #28C840; }
|
||||
.tb-title { font-size: 12.5px; color: var(--dim); }
|
||||
.tb-title b { color: var(--fg); font-weight: 550; }
|
||||
.tb-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--dim); }
|
||||
.pill { background: var(--panel2); border: 1px solid var(--line); border-radius: 20px; padding: 3px 11px;
|
||||
display: flex; align-items: center; gap: 6px; }
|
||||
.pill .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
|
||||
.body { flex: 1; display: flex; min-height: 0; }
|
||||
|
||||
/* ========== ПАНЕЛЬ 1: чаты ========== */
|
||||
.p1 { width: 268px; flex: 0 0 268px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; }
|
||||
.p-head { padding: 14px 15px 9px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.p-head h2 { font-size: 11px; letter-spacing: 1.3px; text-transform: uppercase; color: var(--dim); font-weight: 650; }
|
||||
.p-head button { background: var(--accent); border: none; color: #fff; border-radius: 7px;
|
||||
width: 24px; height: 24px; cursor: pointer; font-size: 15px; line-height: 1; }
|
||||
.f-tabs { display: flex; gap: 4px; padding: 0 12px 10px; flex-wrap: wrap; }
|
||||
.f-tab { font-size: 11px; padding: 4px 9px; border-radius: 7px; color: var(--dim); background: var(--panel2);
|
||||
cursor: pointer; border: 1px solid transparent; }
|
||||
.f-tab.on { color: var(--fg); border-color: var(--accent); background: rgba(124,140,248,.14); }
|
||||
.q { margin: 0 12px 9px; padding: 7px 11px; border-radius: 8px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 12.5px; outline: none; }
|
||||
.q:focus { border-color: var(--accent); } .q::placeholder { color: var(--dim); }
|
||||
.cf { flex: 1; overflow-y: auto; padding: 0 8px 12px; }
|
||||
.c { display: flex; gap: 10px; padding: 9px 10px; border-radius: 9px; cursor: pointer; }
|
||||
.c:hover { background: var(--panel2); }
|
||||
.c.on { background: var(--panel2); box-shadow: inset 2px 0 0 var(--accent); }
|
||||
.c .a { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 9px; display: grid; place-items: center;
|
||||
font-size: 11.5px; font-weight: 700; background: var(--panel3); color: var(--accent); }
|
||||
.c .m { flex: 1; min-width: 0; }
|
||||
.c .n { font-size: 13px; font-weight: 550; display: flex; justify-content: space-between; gap: 6px; }
|
||||
.c .n span.t { font-size: 10px; color: var(--dim); font-weight: 400; }
|
||||
.c .p { font-size: 11.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
|
||||
.c.new .n { color: var(--accent); }
|
||||
|
||||
/* ========== ПАНЕЛЬ 2: диалог ========== */
|
||||
.p2 { flex: 1; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--line); }
|
||||
.p2h { height: 54px; flex: 0 0 54px; display: flex; align-items: center; gap: 11px; padding: 0 18px;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.p2h .nm { font-size: 14px; font-weight: 600; }
|
||||
.p2h .sub { font-size: 11px; color: var(--dim); }
|
||||
.p2h .acts { margin-left: auto; display: flex; gap: 6px; }
|
||||
.p2h .acts button { background: var(--panel2); border: 1px solid var(--line); color: var(--dim);
|
||||
border-radius: 7px; padding: 5px 11px; font-size: 11.5px; cursor: pointer; }
|
||||
.p2h .acts button:hover { color: var(--fg); }
|
||||
|
||||
.flow { flex: 1; overflow-y: auto; padding: 18px 22px; display: flex; flex-direction: column; gap: 11px; }
|
||||
.u { align-self: flex-end; max-width: 78%; background: #2A3350; padding: 9px 13px; border-radius: 12px 12px 3px 12px;
|
||||
font-size: 13.5px; }
|
||||
.a { align-self: flex-start; max-width: 78%; background: var(--panel2); padding: 9px 13px;
|
||||
border-radius: 12px 12px 12px 3px; font-size: 13.5px; }
|
||||
.tm { font-size: 10px; color: var(--dim); margin-top: 4px; }
|
||||
|
||||
/* свёрнутый шаг работы агента — то, что отличает агента от чата */
|
||||
.step { align-self: flex-start; width: 100%; max-width: 92%; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
||||
.step-h { display: flex; align-items: center; gap: 9px; padding: 8px 12px; cursor: pointer; font-size: 12.5px; }
|
||||
.step-h:hover { background: var(--panel2); }
|
||||
.step-h .ico { color: var(--accent); }
|
||||
.step-h .nm { color: var(--fg); font-weight: 550; }
|
||||
.step-h .stat { margin-left: auto; font-size: 10.5px; color: var(--ok); }
|
||||
.step-h .chev { color: var(--dim); font-size: 10px; }
|
||||
.step-b { display: none; padding: 0 12px 11px 34px; font-size: 12px; color: var(--dim); }
|
||||
.step.open .step-b { display: block; }
|
||||
.step-b code { color: #A8B6C8; background: var(--bg); border: 1px solid var(--line); border-radius: 5px;
|
||||
padding: 1px 6px; font-size: 11px; }
|
||||
|
||||
/* ========== ПАНЕЛЬ 3: состояние агента ========== */
|
||||
.p3 { width: 306px; flex: 0 0 306px; background: var(--panel); display: flex; flex-direction: column; }
|
||||
.sec { border-bottom: 1px solid var(--line); padding: 13px 15px; }
|
||||
.sec h3 { font-size: 10.5px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--dim);
|
||||
font-weight: 650; margin-bottom: 10px; display: flex; align-items: center; gap: 7px; }
|
||||
.kv { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 7px; }
|
||||
.kv span:first-child { color: var(--dim); }
|
||||
.kv span:last-child { font-weight: 550; }
|
||||
.bar { height: 5px; border-radius: 3px; background: var(--panel3); overflow: hidden; margin-top: 4px; }
|
||||
.bar i { display: block; height: 100%; background: var(--accent); }
|
||||
.tool-chip { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px 0; }
|
||||
.tool-chip .s { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.tool-chip .s.run { background: var(--warn); animation: p 1s infinite; }
|
||||
.tool-chip .s.ok { background: var(--ok); }
|
||||
@keyframes p { 50% { opacity: .3; } }
|
||||
.mem { font-size: 11.5px; color: var(--dim); padding: 7px 9px; background: var(--panel2);
|
||||
border-radius: 8px; margin-bottom: 6px; line-height: 1.4; }
|
||||
.mem b { color: var(--fg); font-weight: 550; display: block; font-size: 11.5px; margin-bottom: 2px; }
|
||||
|
||||
/* ========== КОМПОЗЕР (низ панели 2) ========== */
|
||||
.comp { border-top: 1px solid var(--line); background: var(--panel); padding: 13px 18px 14px; }
|
||||
.rec-state { display: none; align-items: center; gap: 12px; background: var(--bg); border: 1px solid var(--hot);
|
||||
border-radius: 10px; padding: 10px 13px; margin-bottom: 10px; }
|
||||
.rec-state.on { display: flex; }
|
||||
.stop-rec { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 50%; background: var(--hot);
|
||||
border: none; color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 12px; }
|
||||
.sq { width: 11px; height: 11px; background: #fff; border-radius: 2px; }
|
||||
.meter { flex: 1; display: flex; align-items: center; gap: 3px; height: 26px; }
|
||||
.meter i { width: 3px; background: var(--hot); border-radius: 2px; height: 25%; animation: mn .9s infinite ease-in-out; }
|
||||
.meter i:nth-child(2n) { animation-delay: .12s; } .meter i:nth-child(3n) { animation-delay: .24s; }
|
||||
@keyframes mn { 0%,100% { height: 20%; } 50% { height: 100%; } }
|
||||
.rt { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--hot); font-weight: 600; }
|
||||
.rec-state .live-txt { font-size: 11.5px; color: var(--dim); max-width: 260px; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.crow { display: flex; align-items: flex-end; gap: 8px; }
|
||||
.cbox { flex: 1; display: flex; align-items: flex-end; background: var(--bg); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 9px 12px; }
|
||||
.cbox:focus-within { border-color: var(--accent); }
|
||||
.cbox textarea { flex: 1; background: transparent; border: none; outline: none; color: var(--fg);
|
||||
font-family: inherit; font-size: 13.5px; resize: none; height: 21px; max-height: 110px; line-height: 1.45; }
|
||||
.cbox textarea::placeholder { color: var(--dim); }
|
||||
.ic { width: 36px; height: 36px; border-radius: 9px; border: 1px solid var(--line); background: var(--bg);
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 15px; flex: 0 0 36px; }
|
||||
.ic:hover { color: var(--fg); }
|
||||
.ic.rec { border-color: var(--accent); color: var(--accent); }
|
||||
.ic.rec.live { background: var(--hot); border-color: var(--hot); color: #fff; animation: rp 1.4s infinite; }
|
||||
@keyframes rp { 0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,.5); } 50% { box-shadow: 0 0 0 9px rgba(248,113,113,0); } }
|
||||
.send { height: 36px; padding: 0 16px; border-radius: 9px; border: none; background: var(--accent);
|
||||
color: #fff; font-size: 12.5px; font-weight: 600; cursor: pointer; }
|
||||
.send:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
|
||||
.keys { font-size: 10.5px; color: var(--dim); margin-top: 9px; display: flex; gap: 14px; }
|
||||
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
|
||||
|
||||
/* окно целиком влезает на любой экран — не режем картинку */
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
|
||||
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
|
||||
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="win">
|
||||
<div class="titlebar">
|
||||
<div class="lights"><i></i><i></i><i></i></div>
|
||||
<div class="tb-title">agentik-desktop — <b>agentik · рабочий</b></div>
|
||||
<div class="tb-right">
|
||||
<span class="pill"><span class="d"></span> агент на связи</span>
|
||||
<span>Память: 128 заметок</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<!-- ======== ПАНЕЛЬ 1 ======== -->
|
||||
<aside class="p1">
|
||||
<div class="p-head"><h2>Диалоги</h2><button title="Новый">+</button></div>
|
||||
<div class="f-tabs">
|
||||
<div class="f-tab on">Все</div><div class="f-tab">Работа</div>
|
||||
<div class="f-tab">Инфра</div><div class="f-tab">Дом</div><div class="f-tab">+</div>
|
||||
</div>
|
||||
<input class="q" placeholder="Поиск диалога…">
|
||||
<div class="cf">
|
||||
<div class="c on"><div class="a">АГ</div><div class="m">
|
||||
<div class="n"><span>agentik · рабочий</span><span class="t">14:22</span></div>
|
||||
<div class="p">Собрал отчёт, жду правок</div></div></div>
|
||||
<div class="c new"><div class="a">ОЧ</div><div class="m">
|
||||
<div class="n"><span>Отчёт за квартал</span><span class="t">12:05</span></div>
|
||||
<div class="p">Файл лежит на диске</div></div></div>
|
||||
<div class="c"><div class="a">РЗ</div><div class="m">
|
||||
<div class="n"><span>Разбор логов nginx</span><span class="t">вчера</span></div>
|
||||
<div class="p">Нашёл 3 ошибки 502</div></div></div>
|
||||
<div class="c"><div class="a">ПО</div><div class="m">
|
||||
<div class="n"><span>Solana-бэкенд</span><span class="t">вчера</span></div>
|
||||
<div class="p">Девайс-нет стартовал</div></div></div>
|
||||
<div class="c"><div class="a">КН</div><div class="m">
|
||||
<div class="n"><span>Книги</span><span class="t">пн</span></div>
|
||||
<div class="p">Разложил по полкам</div></div></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ======== ПАНЕЛЬ 2 ======== -->
|
||||
<section class="p2">
|
||||
<div class="p2h">
|
||||
<div>
|
||||
<div class="nm">agentik · рабочий</div>
|
||||
<div class="sub">codding-big · контекст 34%</div>
|
||||
</div>
|
||||
<div class="acts"><button>Стоп</button><button>Очистить</button></div>
|
||||
</div>
|
||||
|
||||
<div class="flow" id="flow">
|
||||
<div class="u">Разберись, почему CI красный, и выпусти релиз 3<span class="tm">14:05</span></div>
|
||||
|
||||
<div class="step open" onclick="this.classList.toggle('open')">
|
||||
<div class="step-h"><span class="ico">🔍</span><span class="nm">Поиск причины</span>
|
||||
<span class="stat">готово · 4 с</span><span class="chev">▼</span></div>
|
||||
<div class="step-b">Нашёл: в именах тестов длинное тире, сборка идёт в ASCII.<br>
|
||||
<code>InvalidPathException: unmappable characters</code></div>
|
||||
</div>
|
||||
|
||||
<div class="step" onclick="this.classList.toggle('open')">
|
||||
<div class="step-h"><span class="ico">✎</span><span class="nm">Правка 16 имён тестов</span>
|
||||
<span class="stat">готово · 12 с</span><span class="chev">▼</span></div>
|
||||
<div class="step-b">Заменено на обычный дефис в 4 файлах. Ничего другого не тронуто.</div>
|
||||
</div>
|
||||
|
||||
<div class="step" onclick="this.classList.toggle('open')">
|
||||
<div class="step-h"><span class="ico">⚙</span><span class="nm">Сборка и проверка</span>
|
||||
<span class="stat">готово · 1 м 10 с</span><span class="chev">▼</span></div>
|
||||
<div class="step-b">14 модулей, тесты зелёные, jar-ы собраны.</div>
|
||||
</div>
|
||||
|
||||
<div class="a">Готово. CI зелёный, релиз «3» опубликован: 101 артефакт в Nexus.<span class="tm">14:22</span></div>
|
||||
</div>
|
||||
|
||||
<div class="comp">
|
||||
<div class="rec-state" id="recState">
|
||||
<button class="stop-rec" onclick="stopRec(false)"><span class="sq"></span></button>
|
||||
<div class="meter" id="meter"></div>
|
||||
<span class="live-txt" id="liveTxt">слушаю…</span>
|
||||
<span class="rt" id="rt">0:00</span>
|
||||
<button class="ic" onclick="stopRec(true)" title="Отменить">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="crow">
|
||||
<div class="cbox"><textarea id="ta" placeholder="Сообщение агенту…"></textarea></div>
|
||||
<button class="ic rec" id="recBtn" title="Голосовое (Ctrl+R)">🎙</button>
|
||||
<button class="send" id="sendBtn" disabled>Отправить</button>
|
||||
</div>
|
||||
<div class="keys">
|
||||
<span><kbd>Ctrl</kbd>+<kbd>R</kbd> запись</span>
|
||||
<span><kbd>Enter</kbd> отправить</span>
|
||||
<span><kbd>Shift</kbd>+<kbd>Enter</kbd> перенос</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======== ПАНЕЛЬ 3 ======== -->
|
||||
<aside class="p3">
|
||||
<div class="sec">
|
||||
<h3>Сессия</h3>
|
||||
<div class="kv"><span>Модель</span><span>codding-big</span></div>
|
||||
<div class="kv"><span>Контекст</span><span>34 %</span></div>
|
||||
<div class="bar"><i style="width:34%"></i></div>
|
||||
<div class="kv" style="margin-top:9px"><span>Ходов</span><span>18</span></div>
|
||||
<div class="kv"><span>Стоимость</span><span>0.42 ₽</span></div>
|
||||
</div>
|
||||
|
||||
<div class="sec">
|
||||
<h3>Инструменты</h3>
|
||||
<div class="tool-chip"><span class="s ok"></span> shell · <span style="color:var(--dim)">готов</span></div>
|
||||
<div class="tool-chip"><span class="s ok"></span> read_file · <span style="color:var(--dim)">готов</span></div>
|
||||
<div class="tool-chip"><span class="s run"></span> write_file · <span style="color:var(--dim)">работает</span></div>
|
||||
<div class="tool-chip"><span class="s ok"></span> memory · <span style="color:var(--dim)">готов</span></div>
|
||||
</div>
|
||||
|
||||
<div class="sec">
|
||||
<h3>Память агента</h3>
|
||||
<div class="mem"><b>Про CI agentik</b>длинное тире в именах тестов ломает сборку в ASCII</div>
|
||||
<div class="mem"><b>Про Nexus</b>публикация идёт через общий action devops/publish</div>
|
||||
</div>
|
||||
|
||||
<div class="sec" style="border-bottom:none">
|
||||
<h3>Файлы сессии</h3>
|
||||
<div class="kv"><span>standalone-3.jar</span><span style="color:var(--dim)">240 МБ</span></div>
|
||||
<div class="kv"><span>agentik-cli-3.jar</span><span style="color:var(--dim)">7.2 МБ</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ta = document.getElementById('ta'), sendBtn = document.getElementById('sendBtn');
|
||||
const flow = document.getElementById('flow');
|
||||
const recBtn = document.getElementById('recBtn'), recState = document.getElementById('recState');
|
||||
const liveTxt = document.getElementById('liveTxt');
|
||||
document.getElementById('meter').innerHTML = Array.from({length: 34}, () => '<i></i>').join('');
|
||||
|
||||
ta.addEventListener('input', () => {
|
||||
sendBtn.disabled = ta.value.trim() === '';
|
||||
ta.style.height = '21px'; ta.style.height = Math.min(ta.scrollHeight, 110) + 'px';
|
||||
});
|
||||
function send() {
|
||||
const t = ta.value.trim(); if (!t) return;
|
||||
add(t, 'u'); ta.value = ''; ta.style.height = '21px'; sendBtn.disabled = true;
|
||||
setTimeout(() => {
|
||||
const s = document.createElement('div'); s.className = 'step';
|
||||
s.onclick = () => s.classList.toggle('open');
|
||||
s.innerHTML = '<div class="step-h"><span class="ico">⚙</span><span class="nm">Выполняю</span>' +
|
||||
'<span class="stat">идёт…</span><span class="chev">▼</span></div>' +
|
||||
'<div class="step-b">Задача: ' + t + '</div>';
|
||||
flow.appendChild(s);
|
||||
setTimeout(() => {
|
||||
s.querySelector('.stat').textContent = 'готово · 3 с';
|
||||
s.querySelector('.stat').style.color = 'var(--ok)';
|
||||
s.classList.add('open');
|
||||
add('Сделано: ' + t, 'a');
|
||||
}, 1400);
|
||||
}, 300);
|
||||
}
|
||||
function add(text, who) {
|
||||
const d = document.createElement('div'); d.className = who;
|
||||
const n = new Date();
|
||||
d.innerHTML = text + '<span class="tm">' + n.getHours() + ':' + String(n.getMinutes()).padStart(2,'0') + '</span>';
|
||||
flow.appendChild(d); flow.scrollTop = flow.scrollHeight;
|
||||
}
|
||||
sendBtn.onclick = send;
|
||||
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
|
||||
|
||||
const P = 'разберись с красным си ай и выпусти новый релиз'.split(' ');
|
||||
let tm = null, sec = 0, wi = 0;
|
||||
function startRec() {
|
||||
sec = 0; wi = 0; recState.classList.add('on'); recBtn.classList.add('live');
|
||||
tm = setInterval(() => {
|
||||
sec++; document.getElementById('rt').textContent = '0:' + String(sec).padStart(2,'0');
|
||||
if (wi < P.length) liveTxt.textContent = P.slice(0, ++wi).join(' ');
|
||||
}, 500);
|
||||
}
|
||||
function stopRec(cancel) {
|
||||
clearInterval(tm); recState.classList.remove('on'); recBtn.classList.remove('live');
|
||||
if (cancel) return;
|
||||
const t = liveTxt.textContent;
|
||||
if (t && sec >= 2) { add(t, 'u'); setTimeout(() => add('Принято голосом, начинаю.', 'a'), 800); }
|
||||
}
|
||||
recBtn.onclick = () => recState.classList.contains('on') ? stopRec(false) : startRec();
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); recBtn.click(); }
|
||||
if (e.key === 'Escape' && recState.classList.contains('on')) stopRec(true);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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.
|
||||
|
||||
## Плюс: список диалогов с двумя агентами
|
||||
Диалоги сгруппированы по агентам, у группы цветная точка, у каждой строки цветная
|
||||
полоска, цвет аватара совпадает с цветом агента из настроек.
|
||||
|
||||
## Что слабо / открытые вопросы
|
||||
- Одного цвета для различения может оказаться мало.
|
||||
- Где хранится список агентов — не решено.
|
||||
- Пароля/ключа доступа у библиотеки нет — поля для него нет.
|
||||
@@ -1,625 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Настройки: агенты</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
/* ============ фон: обычный клиент ============ */
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* ── папки: как в выбранном варианте 1 ── */
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; align-items: flex-start; cursor: pointer; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
||||
.row-tail { flex: 0 0 auto; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
|
||||
border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.ic-btn { width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 16px; margin-left: auto; }
|
||||
.ic-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ ЗАТЕМНЕНИЕ (одно на оба окна) ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; display: none; }
|
||||
.scrim.on { display: block; }
|
||||
.scrim.top { background: rgba(0,0,0,.42); z-index: 200; }
|
||||
|
||||
/* ============ ОКНО НАСТРОЕК ============ */
|
||||
.layer { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; pointer-events: none; }
|
||||
.layer.on { display: flex; }
|
||||
.layer > * { pointer-events: auto; }
|
||||
/* порядок слоёв: настройки 150, проверка поверх 250 */
|
||||
#layerSettings { z-index: 150; }
|
||||
#layerCheck { z-index: 250; }
|
||||
|
||||
.dialog {
|
||||
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px; background: #17212B;
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* список агентов */
|
||||
.agent-card {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.agent-card:hover { border-color: var(--line); }
|
||||
.agent-card .avatar { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
|
||||
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
|
||||
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
|
||||
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* форма */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
.note code { background: var(--panel2); padding: 1px 4px; border-radius: 4px; font-size: 10.5px; }
|
||||
|
||||
/* адрес + кнопка проверки в одной строке */
|
||||
.addr-row { display: flex; gap: 8px; }
|
||||
.addr-row input { flex: 1; }
|
||||
|
||||
.colors { display: flex; gap: 9px; }
|
||||
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
|
||||
.swatch.on { border-color: var(--fg); }
|
||||
|
||||
/* цвет ИЛИ картинка */
|
||||
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
|
||||
.appearance-tabs .tab {
|
||||
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.pane { display: none; }
|
||||
.pane.on { display: block; }
|
||||
|
||||
.pic-row { display: flex; align-items: center; gap: 13px; }
|
||||
.pic-preview {
|
||||
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
|
||||
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
|
||||
font-size: 20px; overflow: hidden;
|
||||
}
|
||||
.pic-preview.filled { border-style: solid; border-color: var(--line); }
|
||||
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.avatar.pic { background: transparent; padding: 0; overflow: hidden; }
|
||||
.avatar.pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
/* кнопки */
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.prim:disabled { background: var(--panel2); border-color: var(--line); color: var(--dim); cursor: default; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ============ ОКНО ПРОВЕРКИ СВЯЗИ ============ */
|
||||
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
|
||||
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
|
||||
@keyframes sp { to { transform: rotate(360deg); } }
|
||||
|
||||
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
|
||||
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-size: 19px; flex: 0 0 40px; }
|
||||
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
|
||||
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
|
||||
.probe-head.busy .big { background: transparent; padding: 0; }
|
||||
.probe-head .txt { flex: 1; }
|
||||
.probe-head .ttl { font-size: 14px; font-weight: 600; }
|
||||
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px; }
|
||||
|
||||
.probe-list { font-size: 12px; }
|
||||
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.probe-list div:last-child { border-bottom: none; }
|
||||
.probe-list span { color: var(--dim); }
|
||||
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; }
|
||||
|
||||
.hint-line { font-size: 11.5px; color: var(--dim); margin-top: 12px; padding: 9px 11px;
|
||||
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); }
|
||||
|
||||
/* переключатель состояний — только для показа */
|
||||
.switcher {
|
||||
position: fixed; top: 14px; left: 50%; transform: translateX(-50%); display: flex; gap: 7px;
|
||||
background: rgba(23,33,43,.96); border: 1px solid var(--line); border-radius: 11px; padding: 7px 9px; z-index: 300;
|
||||
}
|
||||
.switcher button { background: transparent; border: none; color: var(--dim); font-size: 12px;
|
||||
padding: 5px 11px; border-radius: 7px; cursor: pointer; font-family: inherit; }
|
||||
.switcher button.on { background: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
|
||||
/* ============ список диалогов с несколькими агентами ============ */
|
||||
.group-head { display: flex; align-items: center; gap: 8px; padding: 11px 10px 6px; }
|
||||
.group-head .dot { width: 9px; height: 9px; border-radius: 50%; }
|
||||
.group-head .nm { font-size: 10.5px; letter-spacing: 1.1px; text-transform: uppercase; color: var(--dim); font-weight: 700; }
|
||||
.group-head .cnt { font-size: 10.5px; color: var(--dim); margin-left: auto; }
|
||||
.accent-bar { width: 3px; border-radius: 2px; flex: 0 0 3px; align-self: stretch; }
|
||||
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.8); } }
|
||||
@media (max-height: 700px) { .window { transform: scale(.68); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="switcher">
|
||||
<button data-view="list" class="on">Список агентов</button>
|
||||
<button data-view="add">Добавить агента</button>
|
||||
<button data-view="check" data-mode="ok">Проверка: отвечает</button>
|
||||
<button data-view="check" data-mode="err">Проверка: не отвечает</button>
|
||||
<button data-view="multi">Список диалогов</button>
|
||||
</div>
|
||||
|
||||
<div class="window">
|
||||
<!-- ===== фон: клиент с папками ===== -->
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="ic-btn" style="margin:0" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">+</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list" id="chatArea"></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<div class="avatar">АГ</div>
|
||||
<div>
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
<button class="ic-btn" title="Настройки">⚙</button>
|
||||
</header>
|
||||
<div class="messages">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ===== затемнение под окном настроек ===== -->
|
||||
<div class="scrim on" id="scrim1"></div>
|
||||
|
||||
<!-- ===== ОКНО 1: настройки ===== -->
|
||||
<div class="layer on" id="layerSettings">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2 id="dTitle">Агенты</h2>
|
||||
<button class="x-btn" onclick="closeAll()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- ── вид 1: список агентов ── -->
|
||||
<div class="d-body" id="viewList">
|
||||
<div class="agent-card">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state"><span class="d"></span>на связи</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-card">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state err"><span class="d"></span>не отвечает</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Проверить связь">⇄</button>
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn wide" onclick="show('add')">+ Добавить агента</button>
|
||||
</div>
|
||||
|
||||
<!-- ── вид 2: форма ── -->
|
||||
<div class="d-body" id="viewAdd" style="display:none">
|
||||
<div class="field">
|
||||
<label>Название</label>
|
||||
<input type="text" value="рабочий" placeholder="как вы его будете звать">
|
||||
<div class="note">Как называть этого агента в списке. Придумываете сами — сервер своего имени не сообщает.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Адрес сервера агента</label>
|
||||
<div class="addr-row">
|
||||
<input type="text" class="mono" value="http://192.168.88.135:8080/agentik"
|
||||
placeholder="http://хост:порт/agentik">
|
||||
<button class="btn sm" onclick="openCheck('form')">Проверить связь</button>
|
||||
</div>
|
||||
<div class="note">Адрес целиком, вместе с путём <code>/agentik</code>. Если агент
|
||||
на этой же машине — <code>http://localhost:8080/agentik</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Как отличать в списке</label>
|
||||
<div class="appearance-tabs">
|
||||
<div class="tab on" data-tab="color">Цвет</div>
|
||||
<div class="tab" data-tab="pic">Картинка</div>
|
||||
</div>
|
||||
|
||||
<!-- цвет -->
|
||||
<div class="pane on" id="paneColor">
|
||||
<div class="colors">
|
||||
<div class="swatch on" style="background:#6AB2F2"></div>
|
||||
<div class="swatch" style="background:#86C99A"></div>
|
||||
<div class="swatch" style="background:#C9A886"></div>
|
||||
<div class="swatch" style="background:#B79AE0"></div>
|
||||
<div class="swatch" style="background:#E28C8C"></div>
|
||||
</div>
|
||||
<div class="note">Цвет остаётся простым выбором: им помечаются диалоги агента
|
||||
в списке — полоска у строки и точка у группы.</div>
|
||||
</div>
|
||||
|
||||
<!-- картинка -->
|
||||
<div class="pane" id="panePic">
|
||||
<div class="pic-row">
|
||||
<div class="pic-preview" id="picPreview">🖼</div>
|
||||
<div class="pic-actions">
|
||||
<button class="btn sm" onclick="pickPic()">Выбрать файл…</button>
|
||||
<button class="btn sm" id="picClear" onclick="clearPic()" style="display:none">Убрать</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">Картинка копируется <b>внутрь клиента</b> — по исходному
|
||||
адресу не подтягивается. Иначе значок пропадёт, если файл удалят,
|
||||
флешку вынут или папку переименуют.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot" id="footList">
|
||||
<button class="btn prim" style="flex:1" onclick="closeAll()">Готово</button>
|
||||
</div>
|
||||
<div class="d-foot" id="footAdd" style="display:none">
|
||||
<button class="btn" style="flex:1" onclick="show('list')">Отмена</button>
|
||||
<button class="btn prim" style="flex:1">Добавить агента</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== затемнение под окном проверки ===== -->
|
||||
<div class="scrim top" id="scrim2"></div>
|
||||
|
||||
<!-- ===== ОКНО 2: проверка связи (поверх настроек) ===== -->
|
||||
<div class="layer" id="layerCheck">
|
||||
<div class="dialog small">
|
||||
<div class="d-head">
|
||||
<h2>Проверка связи</h2>
|
||||
<button class="x-btn" onclick="closeCheck()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="d-body">
|
||||
<div class="probe-head" id="probeHead">
|
||||
<div class="big" id="probeIcon"></div>
|
||||
<div class="txt">
|
||||
<div class="ttl" id="probeTitle">Спрашиваю агента…</div>
|
||||
<div class="sub" id="probeSub">http://192.168.88.135:8080/agentik</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="probe-list" id="probeList" style="display:none">
|
||||
<div><span>Ответ на запрос</span><b id="p1">—</b></div>
|
||||
<div><span>Отвечает по адресу</span><b id="p2">—</b></div>
|
||||
<div><span>Диалогов у агента</span><b id="p3">—</b></div>
|
||||
<div><span>Отклик</span><b id="p4">—</b></div>
|
||||
</div>
|
||||
|
||||
<div class="hint-line" id="probeHint" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1" id="probeAgain" onclick="runCheck()">Проверить снова</button>
|
||||
<button class="btn prim" style="flex:1" id="probeOk" onclick="closeCheck()" disabled>Добавить агента</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---------------- данные списка диалогов ----------------
|
||||
const CHATS_SINGLE = `
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_MULTI = `
|
||||
<div class="group-head"><span class="dot" style="background:#6AB2F2"></span>
|
||||
<span class="nm">рабочий</span><span class="cnt">2 диалога</span></div>
|
||||
<div class="chat-row active">
|
||||
<div class="accent-bar" style="background:#6AB2F2"></div>
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>CI и релизы</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="accent-bar" style="background:#6AB2F2"></div>
|
||||
<div class="avatar">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-head"><span class="dot" style="background:#86C99A"></span>
|
||||
<span class="nm">домашний</span><span class="cnt">1 диалог</span></div>
|
||||
<div class="chat-row">
|
||||
<div class="accent-bar" style="background:#86C99A"></div>
|
||||
<div class="avatar g2">КН</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Книги и полки</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Разложил по авторам</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const chatArea = document.getElementById('chatArea');
|
||||
chatArea.innerHTML = CHATS_SINGLE;
|
||||
|
||||
// ---------------- окна ----------------
|
||||
function el(id){ return document.getElementById(id); }
|
||||
|
||||
function show(v) {
|
||||
const isList = (v === 'list');
|
||||
el('viewList').style.display = isList ? 'block' : 'none';
|
||||
el('viewAdd').style.display = isList ? 'none' : 'block';
|
||||
el('footList').style.display = isList ? 'flex' : 'none';
|
||||
el('footAdd').style.display = isList ? 'none' : 'flex';
|
||||
el('dTitle').textContent = isList ? 'Агенты' : 'Новый агент';
|
||||
openSettings();
|
||||
}
|
||||
|
||||
function openSettings(){
|
||||
el('scrim1').classList.add('on');
|
||||
el('layerSettings').classList.add('on');
|
||||
}
|
||||
// закрыть окно настроек (вместе с окном проверки, если оно открыто)
|
||||
function closeAll(){
|
||||
el('layerCheck').classList.remove('on');
|
||||
el('scrim2').classList.remove('on');
|
||||
el('layerSettings').classList.remove('on');
|
||||
el('scrim1').classList.remove('on');
|
||||
}
|
||||
function closeCheck(){
|
||||
el('layerCheck').classList.remove('on');
|
||||
el('scrim2').classList.remove('on');
|
||||
}
|
||||
|
||||
// ---------------- ОКНО ПРОВЕРКИ СВЯЗИ ----------------
|
||||
// отдельное окно поверх настроек: из формы нового агента или из списка
|
||||
function openCheck(from, addr) {
|
||||
el('scrim2').classList.add('on');
|
||||
el('layerCheck').classList.add('on');
|
||||
el('probeOk').textContent = (from === 'form') ? 'Добавить агента' : 'Готово';
|
||||
window.__checkAddr = addr || 'http://192.168.88.135:8080/agentik';
|
||||
runCheck();
|
||||
}
|
||||
|
||||
function runCheck() {
|
||||
const head = el('probeHead');
|
||||
const list = el('probeList');
|
||||
const hint = el('probeHint');
|
||||
const icon = el('probeIcon');
|
||||
const addr = window.__checkAddr || 'http://192.168.88.135:8080/agentik';
|
||||
|
||||
head.className = 'probe-head busy';
|
||||
icon.outerHTML = '<span class="spin" id="probeIcon"></span>';
|
||||
el('probeTitle').textContent = 'Спрашиваю агента…';
|
||||
el('probeSub').textContent = addr;
|
||||
list.style.display = 'none';
|
||||
hint.style.display = 'none';
|
||||
el('probeOk').disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
const sp = document.getElementById('probeIcon');
|
||||
sp.outerHTML = '<div class="big" id="probeIcon"></div>';
|
||||
const fail = (window.__checkMode === 'err');
|
||||
|
||||
if (fail) {
|
||||
el('probeHead').className = 'probe-head err';
|
||||
el('probeIcon').textContent = '✕';
|
||||
el('probeTitle').textContent = 'Агент не отвечает';
|
||||
el('probeSub').textContent = addr;
|
||||
el('p1').textContent = 'нет ответа'; el('p2').textContent = '—';
|
||||
el('p3').textContent = '—'; el('p4').textContent = '5000 мс (истекло)';
|
||||
list.style.display = 'block';
|
||||
hint.style.display = 'block';
|
||||
hint.textContent = 'Проверьте адрес и что агент запущен. Убедитесь, что порт открыт и путь /agentik указан целиком.';
|
||||
el('probeOk').disabled = true;
|
||||
} else {
|
||||
el('probeHead').className = 'probe-head ok';
|
||||
el('probeIcon').textContent = '✓';
|
||||
el('probeTitle').textContent = 'Агент отвечает';
|
||||
el('probeSub').textContent = addr;
|
||||
el('p1').textContent = 'ok'; el('p2').textContent = addr;
|
||||
el('p3').textContent = '4'; el('p4').textContent = '38 мс';
|
||||
list.style.display = 'block';
|
||||
hint.style.display = 'block';
|
||||
hint.textContent = 'Адрес рабочий. Можно добавлять этого агента.';
|
||||
el('probeOk').disabled = false;
|
||||
}
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
// ---------------- переключатель для показа ----------------
|
||||
document.querySelectorAll('.switcher button').forEach(b => b.onclick = () => {
|
||||
document.querySelectorAll('.switcher button').forEach(x => x.classList.remove('on'));
|
||||
b.classList.add('on');
|
||||
const v = b.dataset.view;
|
||||
|
||||
if (v === 'multi') {
|
||||
chatArea.innerHTML = CHATS_MULTI;
|
||||
closeAll();
|
||||
return;
|
||||
}
|
||||
chatArea.innerHTML = CHATS_SINGLE;
|
||||
if (v === 'list') { show('list'); closeCheck(); }
|
||||
if (v === 'add') { show('add'); closeCheck(); }
|
||||
if (v === 'check') {
|
||||
window.__checkMode = b.dataset.mode || 'ok';
|
||||
const okAddr = 'http://192.168.88.135:8080/agentik';
|
||||
const badAddr = 'http://192.168.76.199:8080/agentik';
|
||||
show(window.__checkMode === 'ok' ? 'add' : 'list');
|
||||
openCheck('form', window.__checkMode === 'ok' ? okAddr : badAddr);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.swatch').forEach(s => s.onclick = () => {
|
||||
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('on'));
|
||||
s.classList.add('on');
|
||||
});
|
||||
|
||||
// ---------------- как отличать: цвет ИЛИ картинка ----------------
|
||||
document.querySelectorAll('.appearance-tabs .tab').forEach(tab => tab.onclick = () => {
|
||||
document.querySelectorAll('.appearance-tabs .tab').forEach(x => x.classList.remove('on'));
|
||||
tab.classList.add('on');
|
||||
const pic = (tab.dataset.tab === 'pic');
|
||||
el('paneColor').classList.toggle('on', !pic);
|
||||
el('panePic').classList.toggle('on', pic);
|
||||
});
|
||||
|
||||
// выбор картинки: в настоящем клиенте — системный диалог выбора файла,
|
||||
// а выбранный файл КОПИРУЕТСЯ в папку клиента (не ссылка на исходный путь)
|
||||
function pickPic() {
|
||||
// для макета показываем готовый пример вместо файлового диалога
|
||||
setPic('data:image/svg+xml;utf8,' + encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54">
|
||||
<rect width="54" height="54" fill="#86C99A"/>
|
||||
<circle cx="27" cy="21" r="8" fill="#121218"/>
|
||||
<path d="M10 50 Q27 30 44 50 Z" fill="#121218"/>
|
||||
</svg>`));
|
||||
}
|
||||
|
||||
function setPic(src) {
|
||||
const p = el('picPreview');
|
||||
p.innerHTML = `<img src="${src}" alt="">`;
|
||||
p.classList.add('filled');
|
||||
el('picClear').style.display = '';
|
||||
}
|
||||
|
||||
function clearPic() {
|
||||
const p = el('picPreview');
|
||||
p.innerHTML = '🖼';
|
||||
p.classList.remove('filled');
|
||||
el('picClear').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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`), окно и затемнение устроены так же. Ничего нового не изобреталось.
|
||||
@@ -1,436 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Новый диалог — выбор ассистента</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 100vh; padding: 24px; gap: 16px;
|
||||
}
|
||||
|
||||
/* ============ переключатель состояний ============ */
|
||||
.switcher { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; max-width: 1180px; }
|
||||
.switcher button {
|
||||
padding: 7px 13px; border-radius: 9px; border: 1px solid var(--line); background: var(--panel);
|
||||
color: var(--dim); font-size: 12.5px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.switcher button:hover { color: var(--fg); }
|
||||
.switcher button.on { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
|
||||
/* ============ окно клиента ============ */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; align-items: flex-start; cursor: pointer; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
/* аватар-картинка: клиент показывает СВОЮ копию файла */
|
||||
.avatar.pic {
|
||||
background: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E") center/cover;
|
||||
}
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
||||
.row-tail { flex: 0 0 auto; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700; border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
/* кнопка «+» рядом с заголовком. Состояние .hidden — при нуле ассистентов */
|
||||
.ic-btn { width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 16px; }
|
||||
.ic-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.add-btn { font-size: 19px; color: var(--accent); }
|
||||
.add-btn.hidden { display: none; }
|
||||
|
||||
/* пустой список */
|
||||
.empty { padding: 44px 26px; text-align: center; }
|
||||
.empty .big { font-size: 34px; opacity: .28; margin-bottom: 12px; }
|
||||
.empty .ttl { font-size: 14px; font-weight: 600; margin-bottom: 7px; }
|
||||
.empty .sub { font-size: 12.5px; color: var(--dim); line-height: 1.55; }
|
||||
.empty .link { color: var(--accent); cursor: pointer; text-decoration: none; border-bottom: 1px dashed var(--accent); }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.chat-head .ic-btn { margin-left: auto; }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ затемнение и окно ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; display: none; }
|
||||
.scrim.on { display: block; }
|
||||
|
||||
.layer { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; z-index: 150; pointer-events: none; }
|
||||
.layer.on { display: flex; }
|
||||
.layer > * { pointer-events: auto; }
|
||||
|
||||
.dialog {
|
||||
width: 480px; min-width: 480px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
|
||||
|
||||
/* ── вариант выбора ассистента ── */
|
||||
.agent-opt {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; cursor: pointer;
|
||||
border: 1.5px solid transparent;
|
||||
}
|
||||
.agent-opt:hover { border-color: #33414F; }
|
||||
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
|
||||
.agent-opt .avatar { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
|
||||
.ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.ag-state.ok { color: var(--ok); }
|
||||
.ag-state.off { color: var(--dim); } .ag-state.off .d { background: var(--dim); }
|
||||
.ag-state.err { color: var(--err); } .ag-state.err .d { background: var(--err); }
|
||||
|
||||
/* кружок-галочка выбора */
|
||||
.pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%; border: 2px solid #3D4A58; }
|
||||
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
|
||||
.agent-opt.on .pick::after {
|
||||
content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 9px;
|
||||
border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg);
|
||||
}
|
||||
|
||||
/* предупреждение под списком */
|
||||
.warn {
|
||||
display: none; gap: 8px; align-items: flex-start; margin-top: 4px; padding: 10px 12px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA;
|
||||
}
|
||||
.warn.on { display: flex; }
|
||||
.warn .i { flex: 0 0 auto; }
|
||||
|
||||
/* кнопки */
|
||||
.btn {
|
||||
padding: 10px 16px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel2);
|
||||
color: var(--fg); font-size: 13.5px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.btn:hover { border-color: #3D4A58; }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
.btn.prim:disabled { background: #253544; border-color: #253544; color: #5C6B7A; cursor: not-allowed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="switcher">
|
||||
<button data-view="pick" class="on">Выбор: 2 ассистента</button>
|
||||
<button data-view="offline">Третий не отвечает</button>
|
||||
<button data-view="one">Ассистент один</button>
|
||||
<button data-view="zero">Ассистентов нет</button>
|
||||
<button data-view="made">Диалог создан</button>
|
||||
</div>
|
||||
|
||||
<div class="window">
|
||||
<!-- ================= СПИСОК ДИАЛОГОВ ================= -->
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="ic-btn add-btn" id="newChatBtn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…" id="searchBox">
|
||||
|
||||
<div class="folders" id="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt" id="cnt">2</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">+</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list" id="chatList"></div>
|
||||
</aside>
|
||||
|
||||
<!-- ================= ЧАТ ================= -->
|
||||
<main class="main" id="mainPane">
|
||||
<header class="chat-head">
|
||||
<div class="avatar" id="headAv">АГ</div>
|
||||
<div>
|
||||
<div class="head-name" id="headName">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
<button class="ic-btn" title="Настройки">⚙</button>
|
||||
</header>
|
||||
<div class="messages" id="msgs">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="scrim" id="scrim"></div>
|
||||
|
||||
<!-- ================= ОКНО: НОВЫЙ ДИАЛОГ ================= -->
|
||||
<div class="layer" id="layerNew">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Новый диалог</h2>
|
||||
<button class="x-btn" onclick="closeDialog()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="d-body">
|
||||
<div class="d-lead" id="lead">В каком ассистенте создать диалог?</div>
|
||||
|
||||
<div id="agentOpts">
|
||||
<div class="agent-opt on" data-id="a1" data-ok="1" data-tag="АГ">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="agent-opt" data-id="a2" data-ok="1" data-tag="ДМ">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="agent-opt" data-id="a3" data-ok="0" data-tag="КА" data-pic="1">
|
||||
<div class="avatar pic"></div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">Кот-ассистент</div>
|
||||
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
|
||||
<div class="ag-state err"><span class="d"></span>не отвечает</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warn" id="warn">
|
||||
<span class="i">⚠</span>
|
||||
<span id="warnText">Ассистент сейчас не отвечает. Диалог создать можно, но
|
||||
сообщение не уйдёт, пока он не поднимется.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1" onclick="closeDialog()">Отмена</button>
|
||||
<button class="btn prim" style="flex:1" id="createBtn" disabled>Создать диалог</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const el = id => document.getElementById(id);
|
||||
|
||||
// ───────── данные состояний ─────────
|
||||
const AG_ALL = `
|
||||
<div class="agent-opt on" data-id="a1" data-ok="1">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
<div class="agent-opt" data-id="a2" data-ok="1">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
<div class="agent-opt" data-id="a3" data-ok="0">
|
||||
<div class="avatar pic"></div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">Кот-ассистент</div>
|
||||
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
|
||||
<div class="ag-state err"><span class="d"></span>не отвечает</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_TWO = `
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>домашний</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_NEW = `
|
||||
<div class="chat-row active">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>домашний</span><span class="chat-time">сейчас</span></div>
|
||||
<div class="chat-prev">Новый диалог</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_EMPTY = `
|
||||
<div class="empty">
|
||||
<div class="big">💬</div>
|
||||
<div class="ttl">Диалогов пока нет</div>
|
||||
<div class="sub">Ассистенты ещё не добавлены.<br>
|
||||
Добавьте первого в <a class="link" onclick="alert('Откроется окно настроек — макет 004-settings-agents')">настройках</a>.</div>
|
||||
</div>`;
|
||||
|
||||
// ───────── управление окном ─────────
|
||||
function setAgents(html) { el('agentOpts').innerHTML = html; bindOpts(); }
|
||||
|
||||
function bindOpts() {
|
||||
document.querySelectorAll('.agent-opt').forEach(o => o.onclick = () => {
|
||||
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
|
||||
o.classList.add('on');
|
||||
el('createBtn').disabled = false;
|
||||
el('warn').classList.toggle('on', o.dataset.ok === '0');
|
||||
});
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
el('scrim').classList.add('on');
|
||||
el('layerNew').classList.add('on');
|
||||
el('createBtn').disabled = false;
|
||||
}
|
||||
function closeDialog() {
|
||||
el('scrim').classList.remove('on');
|
||||
el('layerNew').classList.remove('on');
|
||||
}
|
||||
el('newChatBtn').onclick = openDialog;
|
||||
|
||||
document.querySelectorAll('.switcher button').forEach(b => b.onclick = () => {
|
||||
document.querySelectorAll('.switcher button').forEach(x => x.classList.remove('on'));
|
||||
b.classList.add('on');
|
||||
render(b.dataset.view);
|
||||
});
|
||||
|
||||
function render(v) {
|
||||
closeDialog();
|
||||
el('warn').classList.remove('on');
|
||||
|
||||
if (v === 'pick') {
|
||||
el('chatList').innerHTML = CHATS_TWO;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
setAgents(AG_ALL);
|
||||
el('createBtn').disabled = false;
|
||||
openDialog();
|
||||
}
|
||||
|
||||
if (v === 'offline') {
|
||||
el('chatList').innerHTML = CHATS_TWO;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
setAgents(AG_ALL);
|
||||
const off = document.querySelector('.agent-opt[data-ok="0"]');
|
||||
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
|
||||
off.classList.add('on');
|
||||
el('warn').classList.add('on');
|
||||
el('createBtn').disabled = false;
|
||||
openDialog();
|
||||
}
|
||||
|
||||
if (v === 'one') {
|
||||
el('chatList').innerHTML = CHATS_TWO;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (v === 'zero') {
|
||||
el('chatList').innerHTML = CHATS_EMPTY;
|
||||
el('folders').style.display = 'none'; el('searchBox').style.display = 'none';
|
||||
el('newChatBtn').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (v === 'made') {
|
||||
el('chatList').innerHTML = CHATS_NEW;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
el('headAv').textContent = 'ДМ'; el('headAv').className = 'avatar g2';
|
||||
el('headName').textContent = 'домашний';
|
||||
}
|
||||
}
|
||||
|
||||
bindOpts();
|
||||
render('pick');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
# sketches — макеты основного окна
|
||||
|
||||
**Здесь рабочий черновик основного окна десктопного клиента.** Это не
|
||||
«утверждено» — `approved/` живёт отдельно. Правило: одобренное живёт только
|
||||
там, а в `sketches/` — то, что ещё сравниваем и допиливаем.
|
||||
|
||||
## Раскладка: rail / voice-first
|
||||
|
||||
Узкая рейка иконок слева (диалоги / память / инструменты / новый),
|
||||
посередине список диалогов, справа чат. Голосовое сообщение — главное
|
||||
действие: крупная акцентная кнопка микрофона в композере.
|
||||
|
||||
**Цветовая схема:** тёмная тема, общий набор токенов из `../THEMES.md`.
|
||||
Палитра согласована с Android-клиентом — расширения (`--panel2`, `--line`,
|
||||
`--badge`, `--ok`, `--user-bubble`, `--bot-bubble`, `--a1`..`--a5`) тут
|
||||
уже есть.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Состояние |
|
||||
|---|---|
|
||||
| `style.css` | Общий CSS для всех макетов |
|
||||
| `1-chats.html` | Слева — список диалогов с цветными полосками агентов, справа — пусто («выберите диалог») |
|
||||
| `2-chat.html` | Открытый диалог: сообщения, ход работы агента, композер с полем + микрофон + отправка |
|
||||
| `3-recording.html` | Идёт запись — в композере волна + таймер + ⏸ + ✕ + ➤, одна строка 46 px |
|
||||
| `4-empty.html` | Список пуст, ни одного ассистента не добавлено |
|
||||
| `5-new-chat.html` | Модалка по центру — выбор ассистента из добавленных |
|
||||
| `6-groups.html` | Модалка управления группами: список с действиями «изменить» / «удалить» |
|
||||
| `7-groups-add.html` | Создание группы: имя + пустой список диалогов + кнопка «+ Добавить диалог» |
|
||||
| `8-groups-edit.html` | Переименование + теги уже добавленных диалогов с `✕` + блок «ОПАСНОЕ» с удалением группы |
|
||||
| `9-groups-pick-chat.html` | Выбор диалога для добавления в группу: поиск + чекбоксы по диалогам |
|
||||
|
||||
## Как смотреть
|
||||
|
||||
Открыть файл в браузере — самодостаточный, без сборки и без зависимостей.
|
||||
+1426
File diff suppressed because it is too large
Load Diff
@@ -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; }
|
||||
Reference in New Issue
Block a user