BORROW-FROM-ASSISTENT.md: что берём (приёмы, плагины, версии), что не берём (архитектура)
This commit is contained in:
@@ -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<ByteArray>` — чанки сразу уходят на распознавание,
|
||||||
|
без накопления файла. Микрофон — `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` | Чужой дизайн и окна |
|
||||||
@@ -65,6 +65,10 @@ Compose Desktop, так что пример использования ровн
|
|||||||
из плюшек — выделение текста и контекстное меню. Для отрисовки разметки не нужен,
|
из плюшек — выделение текста и контекстное меню. Для отрисовки разметки не нужен,
|
||||||
но полезен, если понадобится пример.
|
но полезен, если понадобится пример.
|
||||||
|
|
||||||
|
**Границы заимствования — подробнее в `BORROW-FROM-ASSISTENT.md`.** Коротко:
|
||||||
|
из assistent берём приёмы отрисовки, плагины и версии; архитектуру, слои и готовые
|
||||||
|
экраны (`ChatScreen`, `SettingsDialog`, модули `chat-*`) — не берём, у нас своё.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ sketches/003-three-pane-command/index.html # три панели + состо
|
|||||||
sketches/004-settings-agents/index.html # настройки агентов + проверка связи
|
sketches/004-settings-agents/index.html # настройки агентов + проверка связи
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent` (приёмы, плагины, версии),
|
||||||
|
а что не берём (архитектура, экраны, дизайн — своё).
|
||||||
`MARKDOWN-SOURCE.md` — откуда брать готовую отрисовку Markdown (файлы и адреса).
|
`MARKDOWN-SOURCE.md` — откуда брать готовую отрисовку Markdown (файлы и адреса).
|
||||||
`REQUIREMENTS.md` — требования. Статус: накидываем, ни один пункт не обязателен
|
`REQUIREMENTS.md` — требования. Статус: накидываем, ни один пункт не обязателен
|
||||||
к исполнению в том виде, как записан.
|
к исполнению в том виде, как записан.
|
||||||
@@ -44,6 +46,13 @@ sketches/004-settings-agents/index.html # настройки агентов
|
|||||||
→ не отвечает, с подробностями и подсказкой. Добавить агента можно только
|
→ не отвечает, с подробностями и подсказкой. Добавить агента можно только
|
||||||
после успешной проверки.
|
после успешной проверки.
|
||||||
|
|
||||||
|
## Заимствования: только приёмы, не архитектура
|
||||||
|
|
||||||
|
Из репозитория `ai/assistent` берём **приёмы отрисовки, плагины и версии**.
|
||||||
|
Архитектуру, слои, готовые экраны и окна — **не берём**: дизайн у нас свой,
|
||||||
|
нарисованный (см. `sketches/`). Разбор по пунктам — в `BORROW-FROM-ASSISTENT.md`,
|
||||||
|
там же таблица «берём / не берём».
|
||||||
|
|
||||||
## Как смотреть
|
## Как смотреть
|
||||||
|
|
||||||
Открыть в браузере любой из файлов — каждый самодостаточный, без сборки.
|
Открыть в браузере любой из файлов — каждый самодостаточный, без сборки.
|
||||||
|
|||||||
@@ -62,6 +62,11 @@
|
|||||||
(по нажатию или на удержание) — решим позже.
|
(по нажатию или на удержание) — решим позже.
|
||||||
- **R19.** Распознавание речи — **отдельная тема, здесь не решается.** Важно только:
|
- **R19.** Распознавание речи — **отдельная тема, здесь не решается.** Важно только:
|
||||||
где кнопка, что видно во время записи, что происходит после.
|
где кнопка, что видно во время записи, что происходит после.
|
||||||
|
- **R19.1.** **Как читать микрофон — уже известно, берём приём из `ai/assistent`.**
|
||||||
|
Никаких сторонних библиотек: стандартные средства Java, класс
|
||||||
|
`MicrophoneRecorder` (83 строки). Формат — 16 кГц, 16 бит, моно.
|
||||||
|
Наружу отдаёт поток кусочков, а не файл: можно отправлять на распознавание
|
||||||
|
сразу, не дожидаясь конца записи. Подробности — `BORROW-FROM-ASSISTENT.md`, п. 4.
|
||||||
|
|
||||||
## 6. Несколько агентов
|
## 6. Несколько агентов
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user