use quadraui::{
AppLogic, Backend, ChatController, ChatControllerEvent, ChatRole, Reaction, Rect, StyledText,
Theme, UiEvent,
};
const EXCHANGES: &[(&str, &str)] = &[
(
"How do I convert markdown to styled text?",
"\
## Markdown → StyledText
Call `render_markdown_to_styled(input, &theme)`. It returns a
`RenderedMarkdown` with three length-aligned vectors:
- `lines` — one **StyledText** per source line
- `line_text` — plain text, for hit-tests and search
- `line_scales` — heading scale factors (H1=2.0, H2=1.5, H3=1.2)
Then pass the markdown string to `ChatController::push_turn_markdown` and the
controller stores the styled result as a [`ChatTurn`].
> **Tip**: `snake_case` identifiers like render_markdown_to_styled stay
> upright — the adapter honours CommonMark flanking rules.",
),
(
"What markdown does it support today?",
"\
# Supported constructs
Inline **bold**, *italic*, and `inline_code` all render with correct spans.
Headings `#` / `##` / `###` scale up in the GTK backend.
## Lists
1. Numbered items render with a styled marker
2. Both dash and asterisk bullet syntax work
- First bullet item
- Second bullet item with **bold** text
* Third item — asterisk syntax
## Fenced code blocks
```rust
pub fn push_turn_markdown(
&mut self,
role: ChatRole,
markdown: &str,
theme: &Theme,
) {
// internally calls render_markdown_to_styled
}
```
## Links and blockquotes
See [the quadraui docs](https://github.com/example/quadraui) for details.
> This is a blockquote. It renders with a `│` left-bar decoration.
> You can put **bold** and `code` inside a blockquote too.",
),
(
"Anything else I should know?",
"\
### A few rules to remember
- `snake_case` identifiers stay upright — the flanking guard prevents
`foo_bar_baz` from being parsed as italic.
- Arithmetic like a * b * c is left alone (whitespace-flanked `*`).
- Nested lists are deferred — single level only for now.
> The same `AppLogic` impl drives both TUI and GTK backends. The markdown
> adapter runs once; both backends consume `RenderedMarkdown` unchanged.",
),
];
pub struct AiTranscript {
controller: ChatController,
next_exchange: usize,
}
impl AiTranscript {
pub fn new() -> Self {
let mut controller = ChatController::new("ai-transcript");
controller.set_status(StyledText::plain(
"AI transcript · Ctrl+S to send · PgUp/PgDn scroll · Esc quit",
));
controller.set_scrollbar_width(Some(1.0));
controller.push_turn(
ChatRole::System,
StyledText::plain(
"Connected. Press Ctrl+S or Alt+Enter to send the next canned prompt.",
),
);
Self {
controller,
next_exchange: 0,
}
}
fn send_next(&mut self) {
let (prompt, reply_md) = EXCHANGES[self.next_exchange % EXCHANGES.len()];
self.next_exchange += 1;
self.controller
.push_turn(ChatRole::User, StyledText::plain(prompt));
let theme = Theme::default();
self.controller
.push_turn_markdown(ChatRole::Assistant, reply_md, &theme);
}
}
impl Default for AiTranscript {
fn default() -> Self {
Self::new()
}
}
impl AppLogic for AiTranscript {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
let vp = backend.viewport();
let rect = Rect::new(0.0, 0.0, vp.width, vp.height);
self.controller.render(backend, rect);
}
fn handle(&mut self, event: UiEvent, backend: &mut dyn Backend) -> Reaction {
let vp = backend.viewport();
let rect = Rect::new(0.0, 0.0, vp.width, vp.height);
match self.controller.handle(&event, backend, rect) {
ChatControllerEvent::Submit { .. } => {
self.send_next();
Reaction::Redraw
}
ChatControllerEvent::Cancelled => Reaction::Exit,
ChatControllerEvent::Consumed => Reaction::Redraw,
_ => Reaction::Continue,
}
}
}