@alia.onl/sdk 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +25 -3
  2. package/src/components/AliaChatContent.tsx +356 -0
  3. package/src/components/AliaChatInput.tsx +173 -26
  4. package/src/components/AliaChatMessageList.tsx +459 -168
  5. package/src/components/AliaChatScreen.tsx +140 -0
  6. package/src/components/AliaChatSheet.tsx +41 -44
  7. package/src/components/AliaWelcomeMessage.tsx +75 -0
  8. package/src/components/Markdown.tsx +58 -114
  9. package/src/components/PlanPreviewCard.tsx +76 -0
  10. package/src/components/ResearchProgressCard.tsx +159 -0
  11. package/src/components/ui/button.tsx +104 -0
  12. package/src/components/ui/chat-text-input.tsx +164 -0
  13. package/src/components/ui/dropdown-menu.tsx +2 -0
  14. package/src/components/ui/dropdown-menu.web.tsx +355 -0
  15. package/src/components/ui/prompt-input/actions.tsx +28 -0
  16. package/src/components/ui/prompt-input/add-menu.tsx +83 -0
  17. package/src/components/ui/prompt-input/attachments.tsx +183 -0
  18. package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
  19. package/src/components/ui/prompt-input/context.tsx +71 -0
  20. package/src/components/ui/prompt-input/mic-button.tsx +53 -0
  21. package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
  22. package/src/components/ui/prompt-input/submit-button.tsx +49 -0
  23. package/src/components/ui/prompt-input/textarea.tsx +64 -0
  24. package/src/components/ui/text.tsx +29 -0
  25. package/src/hooks/useAliaChat.ts +18 -8
  26. package/src/hooks/useDocumentPicker.ts +37 -0
  27. package/src/hooks/useImagePicker.ts +38 -0
  28. package/src/hooks/useSpeechToText.ts +25 -9
  29. package/src/index.ts +41 -3
  30. package/src/lib/content-utils.ts +23 -0
  31. package/src/lib/keyboard.native.tsx +6 -0
  32. package/src/lib/keyboard.tsx +25 -0
  33. package/src/lib/tool-registry.ts +55 -0
  34. package/src/lib/utils.ts +14 -0
  35. package/src/optional-deps.d.ts +17 -0
  36. package/src/types.ts +27 -10
  37. package/src/components/AliaChatSuggestions.tsx +0 -70
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Full-screen Alia chat component for use as a route/screen in Oxy ecosystem apps.
3
+ *
4
+ * Usage:
5
+ * ```tsx
6
+ * import { AliaChatScreen } from '@alia.onl/sdk';
7
+ *
8
+ * <AliaChatScreen
9
+ * clientContext="Notes app — user is viewing note #42"
10
+ * suggestions={[{ label: 'Summarize', prompt: 'Summarize this note' }]}
11
+ * headerLeft={<BackButton />}
12
+ * />
13
+ * ```
14
+ */
15
+
16
+ import React, { useRef, useState, useEffect, useCallback } from 'react';
17
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
18
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
+ import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
20
+ import { AliaFace, type AliaExpression } from './AliaFace';
21
+ import { useAliaColors } from '../theme';
22
+ import type { WelcomeSuggestion } from './AliaWelcomeMessage';
23
+
24
+ export interface AliaChatScreenProps {
25
+ /** App context injected into system prompt */
26
+ clientContext?: string;
27
+ /** Alia model (default: 'alia-v1') */
28
+ model?: string;
29
+ /** API URL override */
30
+ apiUrl?: string;
31
+ /** Optional header left action (e.g., back button) */
32
+ headerLeft?: React.ReactNode;
33
+ /** Optional header right action (e.g., settings button) */
34
+ headerRight?: React.ReactNode;
35
+ /** Welcome screen greeting */
36
+ welcomeGreeting?: string;
37
+ /** Welcome screen subtitle */
38
+ welcomeSubtitle?: string;
39
+ /** Welcome screen suggestions */
40
+ welcomeSuggestions?: WelcomeSuggestion[];
41
+ }
42
+
43
+ export function AliaChatScreen({
44
+ clientContext,
45
+ model,
46
+ apiUrl,
47
+ headerLeft,
48
+ headerRight,
49
+ welcomeGreeting,
50
+ welcomeSubtitle,
51
+ welcomeSuggestions,
52
+ }: AliaChatScreenProps) {
53
+ const colors = useAliaColors();
54
+ const insets = useSafeAreaInsets();
55
+ const contentRef = useRef<AliaChatContentRef>(null);
56
+
57
+ const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
58
+ const [hasMessages, setHasMessages] = useState(false);
59
+
60
+ // Poll content ref for header state
61
+ useEffect(() => {
62
+ const interval = setInterval(() => {
63
+ if (contentRef.current) {
64
+ setFaceExpression(contentRef.current.faceExpression);
65
+ setHasMessages(contentRef.current.messages.length > 0);
66
+ }
67
+ }, 200);
68
+ return () => clearInterval(interval);
69
+ }, []);
70
+
71
+ const handleClear = useCallback(() => {
72
+ contentRef.current?.clear();
73
+ }, []);
74
+
75
+ return (
76
+ <View style={[styles.container, { backgroundColor: colors.background, paddingTop: insets.top }]}>
77
+ {/* Header */}
78
+ <View style={styles.header}>
79
+ <View style={styles.headerLeft}>
80
+ {headerLeft}
81
+ <AliaFace size={28} expression={faceExpression} />
82
+ <Text style={[styles.headerTitle, { color: colors.text }]}>Alia</Text>
83
+ </View>
84
+ <View style={styles.headerRight}>
85
+ {hasMessages && (
86
+ <TouchableOpacity onPress={handleClear} style={styles.clearButton}>
87
+ <Text style={[styles.clearText, { color: colors.secondaryText }]}>Clear</Text>
88
+ </TouchableOpacity>
89
+ )}
90
+ {headerRight}
91
+ </View>
92
+ </View>
93
+
94
+ {/* Chat content */}
95
+ <AliaChatContent
96
+ ref={contentRef}
97
+ clientContext={clientContext}
98
+ model={model}
99
+ apiUrl={apiUrl}
100
+ welcomeGreeting={welcomeGreeting}
101
+ welcomeSubtitle={welcomeSubtitle}
102
+ welcomeSuggestions={welcomeSuggestions}
103
+ />
104
+ </View>
105
+ );
106
+ }
107
+
108
+ const styles = StyleSheet.create({
109
+ container: {
110
+ flex: 1,
111
+ },
112
+ header: {
113
+ flexDirection: 'row',
114
+ alignItems: 'center',
115
+ justifyContent: 'space-between',
116
+ paddingHorizontal: 16,
117
+ paddingVertical: 12,
118
+ },
119
+ headerLeft: {
120
+ flexDirection: 'row',
121
+ alignItems: 'center',
122
+ gap: 10,
123
+ },
124
+ headerTitle: {
125
+ fontSize: 18,
126
+ fontWeight: '600',
127
+ },
128
+ headerRight: {
129
+ flexDirection: 'row',
130
+ alignItems: 'center',
131
+ gap: 8,
132
+ },
133
+ clearButton: {
134
+ paddingHorizontal: 10,
135
+ paddingVertical: 6,
136
+ },
137
+ clearText: {
138
+ fontSize: 14,
139
+ },
140
+ });
@@ -27,13 +27,10 @@ import Animated, {
27
27
  withTiming,
28
28
  } from 'react-native-reanimated';
29
29
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
30
- import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
31
- import { AliaChatMessageList } from './AliaChatMessageList';
32
- import { AliaChatInput } from './AliaChatInput';
33
- import { AliaChatSuggestions } from './AliaChatSuggestions';
30
+ import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
34
31
  import { AliaFace, type AliaExpression } from './AliaFace';
35
32
  import { useAliaColors } from '../theme';
36
- import type { AliaChatSuggestion } from '../types';
33
+ import type { WelcomeSuggestion } from './AliaWelcomeMessage';
37
34
 
38
35
  const { height: SCREEN_HEIGHT } = Dimensions.get('window');
39
36
 
@@ -46,12 +43,16 @@ const SPRING_CONFIG = {
46
43
  export interface AliaChatSheetProps {
47
44
  /** App context injected into system prompt */
48
45
  clientContext?: string;
49
- /** Quick action suggestions shown when chat is empty */
50
- suggestions?: AliaChatSuggestion[];
51
46
  /** Alia model (default: 'alia-v1') */
52
47
  model?: string;
53
48
  /** API URL override */
54
49
  apiUrl?: string;
50
+ /** Welcome screen greeting */
51
+ welcomeGreeting?: string;
52
+ /** Welcome screen subtitle */
53
+ welcomeSubtitle?: string;
54
+ /** Welcome screen suggestions */
55
+ welcomeSuggestions?: WelcomeSuggestion[];
55
56
  }
56
57
 
57
58
  export interface AliaChatSheetRef {
@@ -60,20 +61,33 @@ export interface AliaChatSheetRef {
60
61
  }
61
62
 
62
63
  export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
63
- ({ clientContext, suggestions = [], model, apiUrl }, ref) => {
64
+ ({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions }, ref) => {
64
65
  const colors = useAliaColors();
65
66
  const isDark = colors.isDark;
66
67
  const insets = useSafeAreaInsets();
67
68
 
68
- // Chat
69
- const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
70
- const { messages, send, isStreaming, clear } = useAliaChat(chatOptions);
69
+ // Content ref for reading state
70
+ const contentRef = useRef<AliaChatContentRef>(null);
71
+ const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
72
+ const [hasMessages, setHasMessages] = useState(false);
71
73
 
72
74
  // Sheet visibility
73
75
  const [rendered, setRendered] = useState(false);
74
76
  const hasClosedRef = useRef(false);
75
77
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
76
78
 
79
+ // Poll content ref for header state (lightweight, only when rendered)
80
+ useEffect(() => {
81
+ if (!rendered) return;
82
+ const interval = setInterval(() => {
83
+ if (contentRef.current) {
84
+ setFaceExpression(contentRef.current.faceExpression);
85
+ setHasMessages(contentRef.current.messages.length > 0);
86
+ }
87
+ }, 200);
88
+ return () => clearInterval(interval);
89
+ }, [rendered]);
90
+
77
91
  // Reanimated shared values
78
92
  const translateY = useSharedValue(SCREEN_HEIGHT);
79
93
  const backdropOpacity = useSharedValue(0);
@@ -200,16 +214,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
200
214
  [insets.top],
201
215
  );
202
216
 
203
- // Derive AliaFace expression from chat state
204
- const faceExpression = useMemo<AliaExpression>(() => {
205
- if (!isStreaming) return 'Idle A';
206
- const lastMsg = messages[messages.length - 1];
207
- if (lastMsg?.role === 'assistant' && lastMsg.toolInvocations?.some(t => t.state === 'call')) return 'Searching A';
208
- if (lastMsg?.thinking) return 'Thinking';
209
- return 'Writing E';
210
- }, [messages, isStreaming]);
211
-
212
- const showSuggestions = messages.length === 0 && suggestions.length > 0;
217
+ const handleClear = useCallback(() => {
218
+ contentRef.current?.clear();
219
+ }, []);
213
220
 
214
221
  if (!rendered) return null;
215
222
 
@@ -256,8 +263,8 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
256
263
  </Text>
257
264
  </View>
258
265
  <View style={styles.headerRight}>
259
- {messages.length > 0 && (
260
- <TouchableOpacity onPress={clear} style={styles.clearButton}>
266
+ {hasMessages && (
267
+ <TouchableOpacity onPress={handleClear} style={styles.clearButton}>
261
268
  <Text
262
269
  style={[
263
270
  styles.clearText,
@@ -286,26 +293,19 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
286
293
  </View>
287
294
  </View>
288
295
 
289
- {/* Suggestions or Messages */}
296
+ {/* Chat content */}
290
297
  <GestureDetector gesture={nativeGesture}>
291
- <View style={styles.chatArea}>
292
- {showSuggestions ? (
293
- <AliaChatSuggestions
294
- suggestions={suggestions}
295
- onSelect={send}
296
- />
297
- ) : (
298
- <AliaChatMessageList
299
- messages={messages}
300
- isStreaming={isStreaming}
301
- scrollOffsetY={scrollOffsetY}
302
- />
303
- )}
304
- </View>
298
+ <AliaChatContent
299
+ ref={contentRef}
300
+ clientContext={clientContext}
301
+ model={model}
302
+ apiUrl={apiUrl}
303
+ scrollOffsetY={scrollOffsetY}
304
+ welcomeGreeting={welcomeGreeting}
305
+ welcomeSubtitle={welcomeSubtitle}
306
+ welcomeSuggestions={welcomeSuggestions}
307
+ />
305
308
  </GestureDetector>
306
-
307
- {/* Input */}
308
- <AliaChatInput onSend={send} isStreaming={isStreaming} />
309
309
  </Animated.View>
310
310
  </GestureDetector>
311
311
  </GestureHandlerRootView>
@@ -384,7 +384,4 @@ const styles = StyleSheet.create({
384
384
  justifyContent: 'center',
385
385
  borderRadius: 16,
386
386
  },
387
- chatArea: {
388
- flex: 1,
389
- },
390
387
  });
@@ -0,0 +1,75 @@
1
+ import { Pressable, StyleSheet, View } from 'react-native';
2
+ import { BlurView } from 'expo-blur';
3
+ import { Text } from './ui/text';
4
+ import Animated, { FadeIn } from 'react-native-reanimated';
5
+ import { AliaFace } from './AliaFace';
6
+ import type { WelcomeSuggestion } from '../types';
7
+
8
+ export type { WelcomeSuggestion };
9
+
10
+ export interface AliaWelcomeMessageProps {
11
+ greeting: string;
12
+ subtitle?: string;
13
+ suggestions?: WelcomeSuggestion[];
14
+ onSuggestionPress?: (text: string) => void;
15
+ faceSize?: number;
16
+ }
17
+
18
+ export function AliaWelcomeMessage({
19
+ greeting,
20
+ subtitle,
21
+ suggestions = [],
22
+ onSuggestionPress,
23
+ faceSize = 64,
24
+ }: AliaWelcomeMessageProps) {
25
+ return (
26
+ <View className="flex-1 items-center justify-center px-4">
27
+ <View className="w-full max-w-2xl">
28
+ {/* Face + Title */}
29
+ <View className="items-start mb-8">
30
+ <View className="mb-4">
31
+ <AliaFace size={faceSize} expression="Greeting" />
32
+ </View>
33
+ <View className="space-y-2">
34
+ <Text className="text-3xl font-bold tracking-tight text-foreground">
35
+ {greeting}
36
+ </Text>
37
+ {subtitle ? (
38
+ <Text className="text-xl font-medium text-muted-foreground">
39
+ {subtitle}
40
+ </Text>
41
+ ) : null}
42
+ </View>
43
+ </View>
44
+
45
+ {/* Suggestion Grid */}
46
+ {suggestions.length > 0 && (
47
+ <Animated.View entering={FadeIn.duration(400)}>
48
+ <View className="flex-row flex-wrap gap-2">
49
+ {suggestions.map((item) => (
50
+ <Pressable
51
+ key={item.id}
52
+ className="flex-1 min-w-[35%] flex-col items-start rounded-3xl border border-border overflow-hidden active:bg-muted/50"
53
+ onPress={() => onSuggestionPress?.(item.description)}
54
+ >
55
+ <BlurView intensity={60} tint="default" style={StyleSheet.absoluteFill} />
56
+ <View className="p-4 w-full">
57
+ <Text className="text-sm font-medium text-surface-foreground mb-1" numberOfLines={1}>
58
+ {item.title}
59
+ </Text>
60
+ <Text
61
+ className="text-xs text-muted-foreground line-clamp-1"
62
+ numberOfLines={1}
63
+ >
64
+ {item.description}
65
+ </Text>
66
+ </View>
67
+ </Pressable>
68
+ ))}
69
+ </View>
70
+ </Animated.View>
71
+ )}
72
+ </View>
73
+ </View>
74
+ );
75
+ }
@@ -1,9 +1,14 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { View, Text, Platform, Linking } from 'react-native';
3
3
  import Markdown from 'react-native-markdown-display';
4
4
  import { useAliaColors } from '../theme';
5
5
 
6
- function createRules(isDark: boolean, textColor: string, monoFont: string) {
6
+ const BODY_TEXT = { fontSize: 16, lineHeight: 28 } as const;
7
+ const HEADING_TEXT = { fontSize: 16, lineHeight: 22 } as const;
8
+ const SANS_FONT = Platform.select({ ios: 'Inter', android: 'Inter', default: 'Inter, sans-serif' })!;
9
+ const MONO_FONT = Platform.select({ ios: 'Courier', android: 'monospace', default: 'monospace' })!;
10
+
11
+ function createRules(isDark: boolean, textColor: string) {
7
12
  return {
8
13
  heading1: (node: any, children: any) => (
9
14
  <Text
@@ -16,7 +21,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
16
21
  heading2: (node: any, children: any) => (
17
22
  <Text
18
23
  key={node.key}
19
- style={{ marginBottom: 8, marginTop: 12, fontWeight: '600', lineHeight: 22, color: textColor }}
24
+ style={{ marginBottom: 8, marginTop: 12, ...HEADING_TEXT, fontWeight: '600', color: textColor }}
20
25
  >
21
26
  {children}
22
27
  </Text>
@@ -24,7 +29,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
24
29
  heading3: (node: any, children: any) => (
25
30
  <Text
26
31
  key={node.key}
27
- style={{ marginBottom: 6, marginTop: 8, fontWeight: '600', lineHeight: 22, color: textColor }}
32
+ style={{ marginBottom: 6, marginTop: 8, ...HEADING_TEXT, fontWeight: '600', color: textColor }}
28
33
  >
29
34
  {children}
30
35
  </Text>
@@ -32,7 +37,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
32
37
  heading4: (node: any, children: any) => (
33
38
  <Text
34
39
  key={node.key}
35
- style={{ marginBottom: 6, marginTop: 8, fontWeight: '500', lineHeight: 22, color: textColor }}
40
+ style={{ marginBottom: 6, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
36
41
  >
37
42
  {children}
38
43
  </Text>
@@ -40,7 +45,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
40
45
  heading5: (node: any, children: any) => (
41
46
  <Text
42
47
  key={node.key}
43
- style={{ marginBottom: 4, marginTop: 8, fontWeight: '500', lineHeight: 22, color: textColor }}
48
+ style={{ marginBottom: 4, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
44
49
  >
45
50
  {children}
46
51
  </Text>
@@ -48,7 +53,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
48
53
  heading6: (node: any, children: any) => (
49
54
  <Text
50
55
  key={node.key}
51
- style={{ marginBottom: 4, marginTop: 8, fontWeight: '500', lineHeight: 22, color: textColor }}
56
+ style={{ marginBottom: 4, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
52
57
  >
53
58
  {children}
54
59
  </Text>
@@ -67,7 +72,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
67
72
  padding: 12,
68
73
  }}
69
74
  >
70
- <Text style={{ fontSize: 13, fontFamily: monoFont }}>{children}</Text>
75
+ <Text style={{ fontSize: 13, fontFamily: MONO_FONT }}>{children}</Text>
71
76
  </View>
72
77
  ) : (
73
78
  <Text
@@ -77,7 +82,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
77
82
  paddingHorizontal: 4,
78
83
  paddingVertical: 1,
79
84
  fontSize: 13,
80
- fontFamily: monoFont,
85
+ fontFamily: MONO_FONT,
81
86
  borderRadius: 3,
82
87
  }}
83
88
  >
@@ -93,8 +98,8 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
93
98
 
94
99
  return (
95
100
  <View key={node.key} style={{ flexDirection: 'row', paddingVertical: 2, paddingLeft: 16 }}>
96
- <Text style={{ marginRight: 8, minWidth: 14, color: '#71717A' }}>{bullet}</Text>
97
- <Text style={{ flex: 1, color: textColor }}>{children}</Text>
101
+ <Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: '#71717A' }}>{bullet}</Text>
102
+ <Text style={{ flex: 1, ...BODY_TEXT, color: textColor }}>{children}</Text>
98
103
  </View>
99
104
  );
100
105
  },
@@ -105,12 +110,12 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
105
110
  <View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
106
111
  ),
107
112
  strong: (node: any, children: any) => (
108
- <Text key={node.key} style={{ fontWeight: '600', color: textColor }}>{children}</Text>
113
+ <Text key={node.key} style={{ fontWeight: '600', ...BODY_TEXT, color: textColor }}>{children}</Text>
109
114
  ),
110
115
  link: (node: any, children: any) => (
111
116
  <Text
112
117
  key={node.key}
113
- style={{ color: isDark ? '#60a5fa' : '#2563eb', textDecorationLine: 'underline' }}
118
+ style={{ color: isDark ? '#60a5fa' : '#2563eb', textDecorationLine: 'underline', ...BODY_TEXT }}
114
119
  onPress={() => {
115
120
  if (node.attributes?.href) Linking.openURL(node.attributes.href);
116
121
  }}
@@ -119,7 +124,7 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
119
124
  </Text>
120
125
  ),
121
126
  paragraph: (node: any, children: any) => (
122
- <Text key={node.key} style={{ marginBottom: 8, color: textColor }}>{children}</Text>
127
+ <Text key={node.key} style={{ marginBottom: 8, ...BODY_TEXT, color: textColor }}>{children}</Text>
123
128
  ),
124
129
  blockquote: (node: any, children: any) => (
125
130
  <View
@@ -157,63 +162,21 @@ function createRules(isDark: boolean, textColor: string, monoFont: string) {
157
162
  };
158
163
  }
159
164
 
160
- export function AliaMarkdown({ content }: { content: string }) {
161
- const colors = useAliaColors();
162
- const isDark = colors.isDark;
163
-
164
- const textColor = isDark ? '#ffffff' : '#0a0a0a';
165
- const sansFont = Platform.select({ ios: 'Inter', android: 'Inter', default: 'Inter, sans-serif' });
166
- const monoFont = Platform.select({ ios: 'Courier', android: 'monospace', default: 'monospace' });
167
-
168
- const customRules = createRules(isDark, textColor, monoFont);
169
-
170
- const markdownStyles = {
171
- body: {
172
- color: textColor,
173
- fontSize: 16,
174
- lineHeight: 28,
175
- fontFamily: sansFont,
176
- },
177
- text: {
178
- color: textColor,
179
- fontSize: 16,
180
- lineHeight: 28,
181
- fontFamily: sansFont,
182
- },
183
- paragraph: {
184
- marginTop: 0,
185
- marginBottom: 8,
186
- },
187
- strong: {
188
- fontWeight: '600' as const,
189
- },
190
- em: {
191
- fontStyle: 'italic' as const,
192
- },
193
- s: {
194
- textDecorationLine: 'line-through' as const,
195
- },
196
- bullet_list_icon: {
197
- marginLeft: 0,
198
- marginRight: 8,
199
- fontSize: 16,
200
- lineHeight: 28,
201
- },
202
- ordered_list_icon: {
203
- marginLeft: 0,
204
- marginRight: 8,
205
- fontSize: 16,
206
- lineHeight: 28,
207
- },
208
- bullet_list_content: {
209
- flex: 1,
210
- },
211
- ordered_list_content: {
212
- flex: 1,
213
- },
165
+ function createStyles(isDark: boolean, textColor: string, borderColor: string, mutedBg: string) {
166
+ return {
167
+ body: { ...BODY_TEXT, color: textColor, fontFamily: SANS_FONT },
168
+ text: { ...BODY_TEXT, color: textColor, fontFamily: SANS_FONT },
169
+ paragraph: { marginTop: 0, marginBottom: 8 },
170
+ strong: { fontWeight: '600' as const },
171
+ em: { fontStyle: 'italic' as const },
172
+ s: { textDecorationLine: 'line-through' as const },
173
+ bullet_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT },
174
+ ordered_list_icon: { marginLeft: 0, marginRight: 8, ...BODY_TEXT },
175
+ bullet_list_content: { flex: 1 },
176
+ ordered_list_content: { flex: 1 },
214
177
  code_inline: {
215
178
  fontSize: 13,
216
- fontFamily: monoFont,
179
+ fontFamily: MONO_FONT,
217
180
  backgroundColor: isDark ? '#27272a' : '#f4f4f5',
218
181
  borderWidth: 0,
219
182
  borderRadius: 3,
@@ -222,7 +185,7 @@ export function AliaMarkdown({ content }: { content: string }) {
222
185
  },
223
186
  code_block: {
224
187
  fontSize: 13,
225
- fontFamily: monoFont,
188
+ fontFamily: MONO_FONT,
226
189
  backgroundColor: isDark ? '#18181b' : '#f4f4f5',
227
190
  borderWidth: 1,
228
191
  borderColor: isDark ? '#3f3f46' : '#e4e4e7',
@@ -231,58 +194,39 @@ export function AliaMarkdown({ content }: { content: string }) {
231
194
  },
232
195
  fence: {
233
196
  fontSize: 13,
234
- fontFamily: monoFont,
197
+ fontFamily: MONO_FONT,
235
198
  backgroundColor: isDark ? '#18181b' : '#f4f4f5',
236
199
  borderWidth: 1,
237
200
  borderColor: isDark ? '#3f3f46' : '#e4e4e7',
238
201
  borderRadius: 6,
239
202
  padding: 12,
240
203
  },
241
- table: {
242
- borderWidth: 1,
243
- borderColor: colors.border,
244
- borderRadius: 6,
245
- marginVertical: 8,
246
- },
247
- thead: {
248
- backgroundColor: colors.muted,
249
- },
250
- th: {
251
- flex: 1,
252
- padding: 8,
253
- fontWeight: '600' as const,
254
- fontSize: 16,
255
- lineHeight: 28,
256
- },
257
- td: {
258
- flex: 1,
259
- padding: 8,
260
- fontSize: 16,
261
- lineHeight: 28,
262
- },
263
- tr: {
264
- borderBottomWidth: 1,
265
- borderColor: colors.border,
266
- flexDirection: 'row' as const,
267
- },
268
- link: {
269
- textDecorationLine: 'underline' as const,
270
- color: isDark ? '#60a5fa' : '#2563eb',
271
- fontSize: 16,
272
- lineHeight: 28,
273
- },
274
- hr: {
275
- backgroundColor: isDark ? '#3f3f46' : '#d4d4d8',
276
- height: 1,
277
- marginVertical: 16,
278
- },
204
+ table: { borderWidth: 1, borderColor, borderRadius: 6, marginVertical: 8 },
205
+ thead: { backgroundColor: mutedBg },
206
+ th: { flex: 1, padding: 8, fontWeight: '600' as const, ...BODY_TEXT },
207
+ td: { flex: 1, padding: 8, ...BODY_TEXT },
208
+ tr: { borderBottomWidth: 1, borderColor, flexDirection: 'row' as const },
209
+ link: { textDecorationLine: 'underline' as const, color: isDark ? '#60a5fa' : '#2563eb', ...BODY_TEXT },
210
+ hr: { backgroundColor: isDark ? '#3f3f46' : '#d4d4d8', height: 1, marginVertical: 16 },
279
211
  heading1: { fontSize: 18, fontWeight: '600' as const },
280
- heading2: { fontSize: 16, fontWeight: '600' as const },
281
- heading3: { fontSize: 16, fontWeight: '600' as const },
282
- heading4: { fontSize: 16, fontWeight: '500' as const },
283
- heading5: { fontSize: 16, fontWeight: '500' as const },
284
- heading6: { fontSize: 16, fontWeight: '500' as const },
212
+ heading2: { ...HEADING_TEXT, fontWeight: '600' as const },
213
+ heading3: { ...HEADING_TEXT, fontWeight: '600' as const },
214
+ heading4: { ...HEADING_TEXT, fontWeight: '500' as const },
215
+ heading5: { ...HEADING_TEXT, fontWeight: '500' as const },
216
+ heading6: { ...HEADING_TEXT, fontWeight: '500' as const },
285
217
  };
218
+ }
219
+
220
+ export function AliaMarkdown({ content }: { content: string }) {
221
+ const colors = useAliaColors();
222
+ const isDark = colors.isDark;
223
+ const textColor = isDark ? '#ffffff' : '#0a0a0a';
224
+
225
+ const customRules = useMemo(() => createRules(isDark, textColor), [isDark]);
226
+ const markdownStyles = useMemo(
227
+ () => createStyles(isDark, textColor, colors.border, colors.muted),
228
+ [isDark, colors.border, colors.muted],
229
+ );
286
230
 
287
231
  return <Markdown rules={customRules} style={markdownStyles}>{content}</Markdown>;
288
232
  }