惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
小众软件
小众软件
美团技术团队
Martin Fowler
Martin Fowler
爱范儿
爱范儿
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
J
Java Code Geeks
B
Blog
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
B
Blog RSS Feed
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building an AI Chat Starter Kit for CMP: ~20 Lines from E...
Nadeem Iqbal · 2026-05-17 · via DEV Community

PromptBar and LlmTypewriter working together in an iPhone simulator — slash commands, @-mentions, attachment chips, and a streaming assistant reply with live Markdown and syntax-highlighted code

TL;DR

I open-sourced two Compose Multiplatform libraries that pair into an AI chat starter kit you can drop into any Android / iOS / Desktop / Web app:

  • 🤖 prompt-bar — the composer (slash commands, mentions, attachments, send/stop)
  • 💬 llm-typewriter — the renderer (Flow<String> streaming, live markdown, progressive syntax highlighting)

The point isn't either lib in isolation. The point is they're built to wire together so you get a ChatGPT-quality streaming chat UI in ~20 lines on every CMP target.

implementation("io.github.nadeemiqbal:prompt-bar:0.1.0")
implementation("io.github.nadeemiqbal:llm-typewriter:0.1.1")

Enter fullscreen mode Exit fullscreen mode


The problem

I was working on a side project — a Kotlin Multiplatform app that wraps an LLM API — and I needed a chat UI. The kind you've used a hundred times: textfield at the bottom with slash commands and @-mentions, attachment chips above it, a Send button that becomes Stop while the assistant streams a reply, markdown-rich responses with syntax-highlighted code blocks.

Two days later I had a half-working prototype, no tests, and a growing TODO list. The CMP ecosystem just didn't have these pieces wired together.

Here's what I found when I went looking:

What I needed What exists today
Chat composer Stream Chat ships a polished AI composer for Android, tied to their commercial backend.
Slash commands + @ mentions in a composer No CMP option I could find. React has good references (Vercel AI Elements is actively working on these).
Streaming typewriter (Flow-based) Typist-CMP and Texty cover typewriter animation for static strings. I needed a Flow-of-String source so tokens paint the moment they arrive from an LLM SDK.
Live progressive markdown rendering No CMP option I could find. Even in React, Vercel's streamdown is the only popular one.
Syntax-highlighted code blocks (built up live) No CMP option I could find — needs an incremental tokenizer.

The existing libraries are good at what they do. What I needed for my project was a different combination — these five things working together as one experience, on every CMP target. So I built it.


What's in prompt-bar

PromptBar is one composable plus a headless PromptBarState.

Inputs you wire:

  • slashCommands: List<SlashCommand> — name + description + hotkey + onSelect lambda. Type / and an autocomplete dropdown opens.
  • mentionProvider: MentionProvidersuspend fun suggest(query: String): List<Mention>. Plug in your contact / file / symbol source. Async by design so you can hit Room / Ktor / system contacts.
  • templates: List<PromptTemplate> — quick-prompt chips above the input. Tap to populate.
  • modelSelector: @Composable () -> Unit — slot for whatever model picker UI fits your app.
  • onVoiceTap: () -> Unit — mic button slot. Library doesn't decode audio (BYO).

State the library owns:

  • text / fieldValue — the textfield content (live token counter / char count derived)
  • attachments: List<PromptAttachment> — chips above the input; addAttachment / removeAttachment
  • sendState: SendStateDisabled / Ready / Sending / Streaming — derived from content, overridable with markSending() / markStreaming() / markReady()
  • selectedModel: ModelOption? — currently-selected model
  • activeTrigger: ActiveTrigger — what autocomplete is currently open

Key design decisions:

  • / and @ only open the dropdown at the start of a line or after whitespace — so email@domain doesn't spuriously trigger mentions.
  • The Send button is one button that morphs visually based on sendState. No "Send disabled until text" + "separate Stop button" UX — one button, four visual states.
  • Smart paste tokenizer for blobs: paste a@x.com, b@y.com (comma- or newline-separated) and pasteTokensAsAttachments splits it into chips.
  • Headless state. PromptBarState can be constructed without composition (handy for ViewModels and tests).

What's in llm-typewriter

StreamingTypewriter takes a Flow<String> of tokens — typically straight from your LLM SDK's streaming API — and reveals them at the cadence dictated by a SpeedCurve.

The Flow-of-String API matters. A typewriter that takes a static String means you have to buffer the entire LLM response before showing anything. With a Flow, the first token paints the moment it arrives — exactly what you want for an LLM chat.

Live progressive Markdown. The renderer re-parses the revealed text every frame using a prefix-stable parser — the same prefix of input always yields the same prefix of tokens. So:

  • **bold mid-stream renders as plain text. The moment ** closes, it flips to bold.
  • Headings (# Title) render once the line completes.
  • Fenced code blocks (the triple-backtick kotlin kind) render progressively as the lines arrive — with syntax highlighting. Code keywords / strings / numbers / comments highlight live, line by line, as the model emits them.

Three speed curves. A fun interface SpeedCurve lets you tune the cadence:

SpeedCurve.Linear    // constant — every char takes the same time
SpeedCurve.EaseOut   // slight stretch on whitespace
SpeedCurve.Natural   // pauses on .!?,;:\n like a human typist

Enter fullscreen mode Exit fullscreen mode

Or write your own:

val excitedTypist = SpeedCurve { base, _, next ->
    if (next == '!') base * 8 else base
}

Enter fullscreen mode Exit fullscreen mode

Other table-stakes things: tap-to-skip (reveal everything immediately on tap), graceful stop-mid-stream (state.stop() shows a "(stopped)" ghost indicator), custom @Composable cursor (block, line, underscore, or anything you want), screen-reader-friendly live region.


The integration — why these are pitched as a pair

Either library alone is useful. Together, they cover a workflow:

@Composable
fun ChatScreen(vm: ChatViewModel) {
    val prompt = rememberPromptBarState()
    val typewriter = rememberStreamingTypewriterState()

    // Send/Stop button auto-syncs with the typewriter's lifecycle.
    LaunchedEffect(typewriter.isStreaming) {
        if (typewriter.isStreaming) prompt.markStreaming()
        else prompt.markReady()
    }

    Column {
        // Your message list ... assistant bubble uses StreamingTypewriter:
        StreamingTypewriter(
            tokens = vm.responseFlow,
            state = typewriter,
            renderer = rememberMarkdownTypewriterRenderer(),
        )

        // The composer:
        PromptBar(
            state = prompt,
            onSend = { vm.send(prompt.outgoing) },
            onStop = { typewriter.stop(); vm.cancelStream() },
            slashCommands = listOf(
                SlashCommand("clear", "Clear conversation") { vm.clear() },
            ),
            mentionProvider = MentionProvider.fromList(vm.contacts),
        )
    }
}

Enter fullscreen mode Exit fullscreen mode

That's the whole integration. Tap Send → message goes out → assistant bubble starts streaming → button morphs to Stop → tap Stop → typewriter freezes mid-token + shows "(stopped)" + button flips back to Send. The polish layer that usually takes a weekend from scratch.


What's next

Both are 0.1.x. Backlog:

  • prompt-bar: server-driven prompt templates, image attachment previews (composable slot for actual bitmap), full prompt-history scrollback, drag-to-reorder chips, command palette mode.
  • llm-typewriter: more languages in the highlighter (Rust, Go, Swift), table rendering, footnotes, custom thinking-block recognition, voice-of-thought style cycling.

Issues / PRs / feedback very welcome.


Try them

implementation("io.github.nadeemiqbal:prompt-bar:0.1.0")
implementation("io.github.nadeemiqbal:llm-typewriter:0.1.1")

Enter fullscreen mode Exit fullscreen mode

Apache 2.0. Android (minSdk 24) · iOS (x64/arm64/sim) · Desktop (JVM 11) · Web (wasmJs).

Built because I needed it. Hopefully saves you a weekend.