Files
agentik-desktop/BORROW-FROM-ASSISTENT.md
T

302 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Что берём из `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. Запись микрофона — код из assistent брать НЕ надо
**Где:** `client/src/main/kotlin/pw/binom/client/audio/MicrophoneRecorder.kt` (83 строки).
Всё на стандартной Java (`javax.sound.sampled`: `AudioFormat`, `AudioSystem`,
`DataLine`, `TargetDataLine`), формат PCM 16 бит / 16 кГц / моно, буфер 4096 байт.
**Но копировать это не нужно.** У нас есть своя готовая библиотека **`mic-kmp`**
(опубликована, версия `1.0.0`), которая делает то же самое, только лучше:
отмена структурная — микрофон освобождается сам через `finally`, без ручного `close()`.
Здесь файл полезен **только как источник приёмов**:
- подтверждает формат, который ждёт распознавание: **16 кГц, моно, PCM 16 бит, little-endian**
(`AudioFormat.Encoding.PCM_SIGNED, sampleRate, 16, 1, 2, sampleRate, false`);
- показывает, что сторонние библиотеки для звука не нужны — хватает стандартной Java;
- отдаёт данные кусками (`ReceiveChannel<ByteArray>`), а не файлом — можно стримить.
Подробности и координаты библиотек — `MIC-ASR-SEARCH.md`.
---
# ЧАСТЬ 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` |
| Приём записи звука (формат 16к/моно, `javax.sound`) | `ChatViewModel`, `config/*`, `cache/*` |
| Зависимость `ru.otpbank.ai:markdown:0.51.0` | Чужой дизайн и окна |
**Микрофон и распознавание — не из assistent.** У нас для этого есть свои
опубликованные библиотеки: `mic-kmp`, `asr-kmp`, `vad-kmp`. Разбор — `MIC-ASR-SEARCH.md`.