From 2b65baa1390259f994e9673adc9f836b0bd7136c Mon Sep 17 00:00:00 2001 From: Porfiry Date: Sat, 19 Sep 2026 01:36:50 +0300 Subject: [PATCH] =?UTF-8?q?BORROW-FROM-ASSISTENT.md:=20=D1=87=D1=82=D0=BE?= =?UTF-8?q?=20=D0=B1=D0=B5=D1=80=D1=91=D0=BC=20(=D0=BF=D1=80=D0=B8=D1=91?= =?UTF-8?q?=D0=BC=D1=8B,=20=D0=BF=D0=BB=D0=B0=D0=B3=D0=B8=D0=BD=D1=8B,=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8),=20=D1=87=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B1=D0=B5=D1=80=D1=91=D0=BC=20(=D0=B0=D1=80?= =?UTF-8?q?=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BORROW-FROM-ASSISTENT.md | 299 +++++++++++++++++++++++++++++++++++++++ MARKDOWN-SOURCE.md | 4 + README.md | 9 ++ REQUIREMENTS.md | 5 + 4 files changed, 317 insertions(+) create mode 100644 BORROW-FROM-ASSISTENT.md diff --git a/BORROW-FROM-ASSISTENT.md b/BORROW-FROM-ASSISTENT.md new file mode 100644 index 0000000..2b3017e --- /dev/null +++ b/BORROW-FROM-ASSISTENT.md @@ -0,0 +1,299 @@ +# Что берём из `ai/assistent`, а что не берём + +**Правило:** из `ai/assistent` берём **только приёмы отрисовки, плагины и версии**. +Архитектуру, слои, окна и дизайн — **не берём**, пишем своё. Дизайн у нас свой, +нарисованный (см. `sketches/`), и он не должен наследовать чужую структуру. + +**Репозиторий:** https://git.binom.pw/ai/assistent, ветка `main`, коммит `2c5b3f9` +**RAW-адреса** (нужен логин, репозиторий приватный): `https://git.binom.pw/ai/assistent/raw/branch/main/<путь>` + +--- + +# ЧАСТЬ 1. ЧТО БЕРЁМ + +## 1. Плагины и версии + +Из `gradle/libs.versions.toml` и `gradle/wrapper/` репозитория assistent: + +| Что | Версия | +|---|---| +| Kotlin | `2.4.0` | +| Compose Multiplatform | `1.7.3` | +| kotlinx-serialization | `1.11.0` | +| kotlinx-coroutines | `1.11.0` | +| Ktor | `3.1.2` | +| kotlin-logging | `7.0.3` | +| ktlint | `14.2.0` | +| JUnit | `5.10.2` | +| Mockito | `5.12.0` | +| log4j | `2.24.3` | +| Gradle | `9.4.1` | + +**Плагины** (модуль десктоп-клиента, `client/build.gradle.kts`): + +```kotlin +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose) + alias(libs.plugins.ktlint) +} +``` + +**Зависимости Compose для десктопа:** + +```kotlin +implementation(compose.desktop.currentOs) +implementation(compose.material3) +implementation(compose.materialIconsExtended) +implementation(libs.kotlinx.coroutines.swing) // ВАЖНО: Dispatchers.Main на десктопе = Swing EDT +``` + +**Репозитории** (без них не соберётся): + +```kotlin +mavenCentral() +mavenLocal() +maven { + url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/") + isAllowInsecureProtocol = true +} +maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +maven("https://maven.google.com") +``` + +**Точка входа приложения:** + +```kotlin +compose.desktop { + application { + mainClass = "<наш пакет>.MainKt" + } +} +``` + +> ⚠️ **Не путать:** в самом agentik версия Kotlin `2.4.20`, а в assistent `2.4.0`. +> Ориентироваться на версию нашего проекта agentik, а не копировать слепо. + +## 2. Отрисовка Markdown + +Отдельный документ: **`MARKDOWN-SOURCE.md`** — 4 файла, разобрано подробно. + +## 3. Приёмы отрисовки (самое ценное) + +Здесь чужие грабли, уже набитые шишки. Каждый пункт — из кода assistent, с указанием места. + +### 3.1. Таблица и бесконечная ширина — Compose падает + +**Где:** `client-shared/.../ui/MarkdownRendering.kt`, функция `renderTable`, строки ~170–258. + +Нельзя класть `Box(horizontalScroll)` внутрь контейнера с бесконечной максимальной +шириной (`wrapContentWidth`) — Compose падает с ошибкой +`measured with an infinity maximum width`. Из комментария в коде: + +- если внешний скролл уже есть — таблица растёт по содержимому (`wrapContentWidth`), + а широкие колонки прокручивает **внешний** скролл; +- если внешнего нет — скролл добавляется **внутрь** таблицы (`fillMaxWidth` + `horizontalScroll`). + +Приём: **параметр `isBubbleScrolled`**, которым таблица переключается между двумя +режимами, вместо одной универсальной раскладки. + +### 3.2. `derivedStateOf` создаёт гонку — читать напрямую + +**Где:** `client/.../ui/ChatScreen.kt`, строки ~1085–1092. + +Из комментария: производное состояние, созданное при рекомпозиции, читается +observer'ом во время фоновых изменений списков — известная гонка Compose +`state created after snapshot was taken`. + +**Приём:** вместо `derivedStateOf { listState.canScrollForward }` писать просто + +```kotlin +val isNotAtBottom = listState.canScrollForward +``` + +Прямое чтение убирает `DerivedSnapshotState` и гонку вместе с ним. + +### 3.3. Ленивый список: `getOrNull` + стабильный ключ + +**Где:** `client/.../ui/ChatScreen.kt`, строки ~1030–1043. + +Список сообщений меняется фоном (подгрузка истории, новые сообщения), поэтому +прямой доступ по индексу может выйти за границы и уронить композицию. + +```kotlin +items( + count = messages.size, + key = { index -> messages.getOrNull(index)?.id ?: "pos:$index" }, +) { index -> + val msg = messages.getOrNull(index) ?: return@items + ... +} +``` + +**Приём:** `count` + `key` + `getOrNull` — вместо `items(list)`. Ключ стабилен при +изменении списка, лишних перерисовок нет. + +### 3.4. Автоскролл вниз при новых сообщениях + +**Где:** `client/.../ui/ChatScreen.kt`, строки ~861–868. + +```kotlin +val listState = rememberLazyListState() +LaunchedEffect(messages.size, isThinking, isTranscribing.value) { + if (messages.isNotEmpty()) listState.scrollToItem(messages.size - 1) +} +``` + +**Приём:** ключи `LaunchedEffect` — то, при изменении чего прокручивать. Плюс +«печатает» и «запись голоса» в ключах: прокрутка случается и при появлении +индикатора, а не только при новом сообщении. Для нас то же: ответ течёт потоком. + +### 3.5. Кнопка «прокрутить вниз» — плавно и без гонки + +**Где:** там же, строки ~1085–1110. + +```kotlin +val isNotAtBottom = listState.canScrollForward +val buttonAlpha by animateFloatAsState( + targetValue = if (isNotAtBottom) 1f else 0f, + animationSpec = tween(200), + label = "scrollBtnAlpha", +) +``` + +Прокрутка — в корутине: `scope.launch { listState.animateScrollToItem(size - 1) }`. +Кнопка проявляется/исчезает плавно, а не мигает. + +### 3.6. Полоса прокрутки, видимая по наведению + +**Где:** там же, строки ~1062–1086. + +```kotlin +VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight() + .alpha(msgScrollbarAlpha).width(8.dp), + adapter = rememberScrollbarAdapter(scrollState = listState), + style = ScrollbarStyle(minimalHeight = 30.dp, thickness = 8.dp, + shape = RoundedCornerShape(4.dp), hoverDurationMillis = 150), +) +``` + +**Приём:** `rememberScrollbarAdapter` привязывает полосу к ленивому списку; ширина +8 dp и появление по наведению — не мозолит глаза. + +### 3.7. Кликабельные ссылки внутри текста + +**Где:** `client-shared/.../ui/MarkdownRendering.kt`, строки ~301–336. + +Приём: `pointerInput` + `detectTapGestures` даёт координаты нажатия; +`onTextLayout` сохраняет `textLayoutResult`; `getOffsetForPosition(offset)` превращает +координаты в позицию в тексте; по позиции ищем аннотацию (ссылка или код) и +открываем её. + +```kotlin +Modifier + .pointerInput(annotatedString) { + detectTapGestures { offset -> + val layout = textLayoutResult ?: return@detectTapGestures + val position = layout.getOffsetForPosition(offset) + ... // найти аннотацию по position + } + } +``` + +**Зачем нам:** ссылки в ответах агента должны открываться в браузере, а не быть +просто текстом. + +### 3.8. Копирование блока кода по кнопке + +**Где:** там же, строки ~90–125. + +```kotlin +val clipboardManager = LocalClipboardManager.current +... +clipboardManager.setText(AnnotatedString(segment.code)) +``` + +**Приём:** `LocalClipboardManager` работает и на десктопе, отдельная библиотека не нужна. + +### 3.9. Выделение текста мышью + +`SelectionContainer` вокруг сообщения — иначе текст не выделить, а для агента это +нужно постоянно. + +### 3.10. Сборка стилей текста + +**Где:** там же, `MarkdownSegment.Text.toAnnotatedString`, строки ~341–377. + +`AnnotatedString.Builder` + построение `SpanStyle` из простого описания +(`bold`/`italic`/`code`/`strikethrough`/`link`). Логику стилей держат в данных, а не +в Композе — удобно и переносимо. + +## 4. Запись микрофона на десктопе — без единой библиотеки + +**Где:** `client/src/main/kotlin/pw/binom/client/audio/MicrophoneRecorder.kt` (83 строки). + +Ценнейшая находка для нашей кнопки записи. Всё на стандартной Java: + +```kotlin +import javax.sound.sampled.AudioFormat +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.DataLine +import javax.sound.sampled.TargetDataLine +``` + +Формат: **PCM 16 бит, 16 кГц, моно, little-endian**, буфер 4096 байт. +Наружу отдаёт `ReceiveChannel` — чанки сразу уходят на распознавание, +без накопления файла. Микрофон — `AudioSystem.getLine(...) as TargetDataLine`. + +**Почему важно:** сторонние библиотеки для звука не нужны вообще. Мы это +собирались решать позже, но приём уже есть и он рабочий. + +--- + +# ЧАСТЬ 2. ЧТО НЕ БЕРЁМ + +## Архитектура и слои — пишем своё + +Не копировать: + +- **`client/src/main/kotlin/pw/binom/client/`** целиком — это чужое приложение: + `Main.kt`, `viewmodel/ChatViewModel.kt`, `config/AppSettings.kt`, `config/ClientConfig.kt`, + `cache/*`. +- **`client/.../ui/ChatScreen.kt`** (1903 строки) — экран чата целиком. Оттуда годятся + только перечисленные выше приёмы, а не структура. +- **`ui/SettingsDialog.kt`** (301) — чужое окно настроек. У нас свой дизайн настроек + (`sketches/004-settings-agents`). +- **`ui/NewChatDialog.kt`** (616), **`ui/ParticipantsDialog.kt`** (223) — чужое. +- **`ui/MessageBubble.kt`** (789) — пузырь сообщения; тянет `chat-common` и чужие модели. +- **Модули `chat-*`** (`chat-common`, `chat-client`, `chat-cache`, `chat-adapter`, + `chat-server-client`) — чужая система чатов. У нас связь с агентом идёт через + **`pw.binom.agentik:client`**, совсем другой протокол. + +**Почему не берём:** у нас другая модель — один клиент к нескольким агентам agentik, +а не чат-сервер с комнатами, участниками и адаптерами. Чужая структура тут только +запутает. + +## Что у нас своё (не из assistent) + +- Раскладка и дизайн — `sketches/001`…`004`. +- Различение агентов цветом или картинкой. +- Проверка связи с агентом отдельным окном. +- Поведение узкого окна (список ↔ чат с кнопкой «назад»). +- Показ хода работы агента (свёрнутые шаги). +- Хранение списка агентов. + +--- + +# Итог одной таблицей + +| Берём | Не берём | +|---|---| +| Версии и плагины, репозитории | Архитектуру и слои | +| Отрисовку Markdown (4 файла) | Экраны целиком (`ChatScreen`, `SettingsDialog`, …) | +| Приёмы: таблицы, ленивый список, автоскролл, полоса прокрутки | Модули `chat-*` и модели сообщений | +| Кликабельные ссылки, копирование кода, выделение текста | `MessageBubble` | +| Запись микрофона через `javax.sound` | `ChatViewModel`, `config/*`, `cache/*` | +| Зависимость `ru.otpbank.ai:markdown:0.51.0` | Чужой дизайн и окна | diff --git a/MARKDOWN-SOURCE.md b/MARKDOWN-SOURCE.md index 28d5e63..79256fd 100644 --- a/MARKDOWN-SOURCE.md +++ b/MARKDOWN-SOURCE.md @@ -65,6 +65,10 @@ Compose Desktop, так что пример использования ровн из плюшек — выделение текста и контекстное меню. Для отрисовки разметки не нужен, но полезен, если понадобится пример. +**Границы заимствования — подробнее в `BORROW-FROM-ASSISTENT.md`.** Коротко: +из assistent берём приёмы отрисовки, плагины и версии; архитектуру, слои и готовые +экраны (`ChatScreen`, `SettingsDialog`, модули `chat-*`) — не берём, у нас своё. + --- ## Зависимости diff --git a/README.md b/README.md index 8059c81..bf8e54a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ sketches/003-three-pane-command/index.html # три панели + состо sketches/004-settings-agents/index.html # настройки агентов + проверка связи ``` +`BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent` (приёмы, плагины, версии), +а что не берём (архитектура, экраны, дизайн — своё). `MARKDOWN-SOURCE.md` — откуда брать готовую отрисовку Markdown (файлы и адреса). `REQUIREMENTS.md` — требования. Статус: накидываем, ни один пункт не обязателен к исполнению в том виде, как записан. @@ -44,6 +46,13 @@ sketches/004-settings-agents/index.html # настройки агентов → не отвечает, с подробностями и подсказкой. Добавить агента можно только после успешной проверки. +## Заимствования: только приёмы, не архитектура + +Из репозитория `ai/assistent` берём **приёмы отрисовки, плагины и версии**. +Архитектуру, слои, готовые экраны и окна — **не берём**: дизайн у нас свой, +нарисованный (см. `sketches/`). Разбор по пунктам — в `BORROW-FROM-ASSISTENT.md`, +там же таблица «берём / не берём». + ## Как смотреть Открыть в браузере любой из файлов — каждый самодостаточный, без сборки. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 5ace134..eecf6a3 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -62,6 +62,11 @@ (по нажатию или на удержание) — решим позже. - **R19.** Распознавание речи — **отдельная тема, здесь не решается.** Важно только: где кнопка, что видно во время записи, что происходит после. +- **R19.1.** **Как читать микрофон — уже известно, берём приём из `ai/assistent`.** + Никаких сторонних библиотек: стандартные средства Java, класс + `MicrophoneRecorder` (83 строки). Формат — 16 кГц, 16 бит, моно. + Наружу отдаёт поток кусочков, а не файл: можно отправлять на распознавание + сразу, не дожидаясь конца записи. Подробности — `BORROW-FROM-ASSISTENT.md`, п. 4. ## 6. Несколько агентов