Skip to content

Commit a2bef06

Browse files
committed
feat(ios): improve GFM streaming for tables and math
1 parent e4741a6 commit a2bef06

18 files changed

Lines changed: 1194 additions & 241 deletions

apps/example/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import { SafeAreaView } from 'react-native-safe-area-context';
1616
import { sampleMarkdown } from './sampleMarkdown';
1717
import { customMarkdownStyle } from './markdownStyles';
1818
import InputScreen from './InputScreen';
19+
import StreamingMarkdownSimulator from './StreamingMarkdownSimulator';
1920

20-
type Screen = 'text' | 'input';
21+
type Screen = 'text' | 'input' | 'stream';
2122

2223
export default function App() {
2324
const [screen, setScreen] = useState<Screen>('input');
@@ -78,10 +79,18 @@ export default function App() {
7879
>
7980
Text
8081
</Text>
82+
<Text
83+
style={[styles.tab, screen === 'stream' && styles.tabActive]}
84+
onPress={() => setScreen('stream')}
85+
>
86+
Stream
87+
</Text>
8188
</View>
8289

8390
{screen === 'input' ? (
8491
<InputScreen />
92+
) : screen === 'stream' ? (
93+
<StreamingMarkdownSimulator />
8594
) : (
8695
<ScrollView
8796
style={styles.scrollView}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
2+
import {
3+
ScrollView,
4+
StyleSheet,
5+
Text,
6+
TouchableOpacity,
7+
View,
8+
} from 'react-native';
9+
import { EnrichedMarkdownText } from 'react-native-enriched-markdown';
10+
import { customMarkdownStyle } from './markdownStyles';
11+
12+
const STREAM_SOURCE = `Here is a tiny streamed answer.
13+
14+
First table:
15+
16+
| Item | Value |
17+
| --- | ---: |
18+
| Alpha | 1 |
19+
| Beta | 2 |
20+
21+
First LaTeX block:
22+
23+
$$
24+
E = mc^2
25+
$$
26+
27+
Second table:
28+
29+
| Step | Status |
30+
| --- | --- |
31+
| Parse | done |
32+
| Render | streaming |
33+
34+
Second LaTeX block:
35+
36+
$$
37+
a^2 + b^2 = c^2
38+
$$
39+
40+
Final table:
41+
42+
| Block | Kind |
43+
| --- | --- |
44+
| One | text |
45+
| Two | table |
46+
| Three | math |
47+
48+
Done.`;
49+
50+
const TICK_MS = 80;
51+
const CHARS_PER_TICK = 3;
52+
53+
export default function StreamingMarkdownSimulator() {
54+
const [cursor, setCursor] = useState(0);
55+
const [isStreaming, setIsStreaming] = useState(false);
56+
const markdownStyle = useMemo(() => customMarkdownStyle, []);
57+
58+
const markdown = STREAM_SOURCE.slice(0, cursor);
59+
const isComplete = cursor >= STREAM_SOURCE.length;
60+
61+
const step = useCallback(() => {
62+
setCursor((current) =>
63+
Math.min(current + CHARS_PER_TICK, STREAM_SOURCE.length)
64+
);
65+
}, []);
66+
67+
const reset = useCallback(() => {
68+
setIsStreaming(false);
69+
setCursor(0);
70+
}, []);
71+
72+
useEffect(() => {
73+
if (!isStreaming || isComplete) {
74+
if (isComplete) {
75+
setIsStreaming(false);
76+
}
77+
return;
78+
}
79+
80+
const interval = setInterval(step, TICK_MS);
81+
return () => clearInterval(interval);
82+
}, [isStreaming, isComplete, step]);
83+
84+
return (
85+
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
86+
<Text style={styles.title}>Streaming markdown simulator</Text>
87+
<Text style={styles.subtitle}>
88+
JS-only stream: short text, a few tables, and a few block LaTeX
89+
segments.
90+
</Text>
91+
92+
<View style={styles.controls}>
93+
<ControlButton
94+
label={isStreaming ? 'Pause' : isComplete ? 'Replay' : 'Start'}
95+
onPress={() => {
96+
if (isComplete) {
97+
setCursor(0);
98+
setIsStreaming(true);
99+
return;
100+
}
101+
setIsStreaming((value) => !value);
102+
}}
103+
/>
104+
<ControlButton label="Step" onPress={step} disabled={isComplete} />
105+
<ControlButton label="Reset" onPress={reset} />
106+
</View>
107+
108+
<Text style={styles.progress}>
109+
{cursor}/{STREAM_SOURCE.length} characters
110+
</Text>
111+
112+
<View style={styles.preview}>
113+
<EnrichedMarkdownText
114+
flavor="github"
115+
markdown={markdown}
116+
markdownStyle={markdownStyle}
117+
md4cFlags={{ latexMath: true }}
118+
streamingAnimation
119+
/>
120+
</View>
121+
122+
<Text style={styles.rawLabel}>Raw streamed markdown</Text>
123+
<Text style={styles.raw}>{markdown || 'Waiting to stream...'}</Text>
124+
</ScrollView>
125+
);
126+
}
127+
128+
function ControlButton({
129+
label,
130+
onPress,
131+
disabled = false,
132+
}: {
133+
label: string;
134+
onPress: () => void;
135+
disabled?: boolean;
136+
}) {
137+
return (
138+
<TouchableOpacity
139+
style={[styles.button, disabled && styles.buttonDisabled]}
140+
onPress={onPress}
141+
disabled={disabled}
142+
>
143+
<Text style={styles.buttonText}>{label}</Text>
144+
</TouchableOpacity>
145+
);
146+
}
147+
148+
const styles = StyleSheet.create({
149+
root: {
150+
flex: 1,
151+
},
152+
content: {
153+
padding: 16,
154+
gap: 12,
155+
},
156+
title: {
157+
fontSize: 20,
158+
fontWeight: '700',
159+
color: '#111827',
160+
},
161+
subtitle: {
162+
fontSize: 14,
163+
color: '#6B7280',
164+
},
165+
controls: {
166+
flexDirection: 'row',
167+
gap: 8,
168+
},
169+
button: {
170+
paddingHorizontal: 14,
171+
paddingVertical: 10,
172+
borderRadius: 8,
173+
backgroundColor: '#2563EB',
174+
},
175+
buttonDisabled: {
176+
backgroundColor: '#9CA3AF',
177+
},
178+
buttonText: {
179+
color: '#FFFFFF',
180+
fontWeight: '600',
181+
},
182+
progress: {
183+
color: '#6B7280',
184+
fontSize: 12,
185+
},
186+
preview: {
187+
padding: 12,
188+
borderWidth: StyleSheet.hairlineWidth,
189+
borderColor: '#D1D5DB',
190+
borderRadius: 12,
191+
backgroundColor: '#FFFFFF',
192+
},
193+
rawLabel: {
194+
marginTop: 8,
195+
color: '#374151',
196+
fontWeight: '600',
197+
},
198+
raw: {
199+
padding: 12,
200+
borderRadius: 8,
201+
backgroundColor: '#F3F4F6',
202+
color: '#111827',
203+
fontFamily: 'Menlo',
204+
fontSize: 12,
205+
},
206+
});

0 commit comments

Comments
 (0)