@alia.onl/sdk 3.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.
- package/package.json +1 -1
- package/src/components/AliaChatContent.tsx +62 -19
- package/src/components/AliaChatMessageList.tsx +437 -279
- package/src/components/AliaChatScreen.tsx +13 -5
- package/src/components/AliaChatSheet.tsx +11 -5
- package/src/components/AliaWelcomeMessage.tsx +75 -0
- package/src/components/Markdown.tsx +58 -114
- package/src/components/PlanPreviewCard.tsx +76 -0
- package/src/components/ResearchProgressCard.tsx +159 -0
- package/src/hooks/useAliaChat.ts +2 -1
- package/src/hooks/useSpeechToText.ts +21 -7
- package/src/index.ts +8 -1
- package/src/lib/content-utils.ts +23 -0
- package/src/lib/tool-registry.ts +55 -0
- package/src/types.ts +20 -7
- package/src/components/AliaChatSuggestions.tsx +0 -70
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, Pressable } from 'react-native';
|
|
3
|
+
import { Text } from './ui/text';
|
|
4
|
+
import {
|
|
5
|
+
Search,
|
|
6
|
+
BookOpen,
|
|
7
|
+
CheckCircle,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronUp,
|
|
10
|
+
ExternalLink,
|
|
11
|
+
Loader2,
|
|
12
|
+
} from 'lucide-react-native';
|
|
13
|
+
import { useAliaColors } from '../theme';
|
|
14
|
+
import type { ResearchProgress } from '../types';
|
|
15
|
+
|
|
16
|
+
interface ResearchProgressCardProps {
|
|
17
|
+
progress: ResearchProgress;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PHASE_LABELS: Record<string, string> = {
|
|
21
|
+
decomposing: 'Planning research',
|
|
22
|
+
searching: 'Searching the web',
|
|
23
|
+
reading: 'Reading sources',
|
|
24
|
+
synthesizing: 'Synthesizing report',
|
|
25
|
+
follow_up: 'Follow-up research',
|
|
26
|
+
finalizing: 'Polishing report',
|
|
27
|
+
complete: 'Research complete',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PHASE_ORDER = ['decomposing', 'searching', 'synthesizing', 'follow_up', 'finalizing', 'complete'];
|
|
31
|
+
|
|
32
|
+
export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
33
|
+
const colors = useAliaColors();
|
|
34
|
+
const [showSources, setShowSources] = useState(false);
|
|
35
|
+
const [showQuestions, setShowQuestions] = useState(true);
|
|
36
|
+
|
|
37
|
+
const phaseIndex = PHASE_ORDER.indexOf(progress.phase || '');
|
|
38
|
+
const isComplete = progress.phase === 'complete' || progress.isComplete;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View className="mx-3 my-2 rounded-xl border border-border bg-card p-4 gap-3">
|
|
42
|
+
{/* Header */}
|
|
43
|
+
<View className="flex-row items-center gap-2">
|
|
44
|
+
<View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center">
|
|
45
|
+
{isComplete ? (
|
|
46
|
+
<CheckCircle size={16} color="#22c55e" />
|
|
47
|
+
) : (
|
|
48
|
+
<Search size={16} color={colors.tint} />
|
|
49
|
+
)}
|
|
50
|
+
</View>
|
|
51
|
+
<View className="flex-1">
|
|
52
|
+
<Text className="text-sm font-semibold text-foreground">
|
|
53
|
+
Deep Research
|
|
54
|
+
</Text>
|
|
55
|
+
<Text className="text-xs text-muted-foreground">
|
|
56
|
+
{progress.message || PHASE_LABELS[progress.phase || ''] || 'Researching...'}
|
|
57
|
+
</Text>
|
|
58
|
+
</View>
|
|
59
|
+
{progress.sourcesFound !== undefined && (
|
|
60
|
+
<View className="bg-muted rounded-full px-2 py-0.5">
|
|
61
|
+
<Text className="text-[10px] text-muted-foreground">
|
|
62
|
+
{progress.sourcesFound} sources
|
|
63
|
+
</Text>
|
|
64
|
+
</View>
|
|
65
|
+
)}
|
|
66
|
+
</View>
|
|
67
|
+
|
|
68
|
+
{/* Phase progress */}
|
|
69
|
+
<View className="flex-row gap-1">
|
|
70
|
+
{PHASE_ORDER.map((phase, i) => (
|
|
71
|
+
<View
|
|
72
|
+
key={phase}
|
|
73
|
+
className="flex-1 h-1 rounded-full"
|
|
74
|
+
style={{
|
|
75
|
+
backgroundColor: i <= phaseIndex
|
|
76
|
+
? (isComplete ? '#22c55e' : colors.tint)
|
|
77
|
+
: colors.muted,
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
</View>
|
|
82
|
+
|
|
83
|
+
{/* Sub-questions */}
|
|
84
|
+
{progress.subQuestions && progress.subQuestions.length > 0 && (
|
|
85
|
+
<View>
|
|
86
|
+
<Pressable
|
|
87
|
+
onPress={() => setShowQuestions(!showQuestions)}
|
|
88
|
+
className="flex-row items-center gap-1 mb-1"
|
|
89
|
+
>
|
|
90
|
+
<BookOpen size={12} color={colors.secondaryText} />
|
|
91
|
+
<Text className="text-xs font-medium text-muted-foreground">
|
|
92
|
+
Research angles ({progress.subQuestions.length})
|
|
93
|
+
</Text>
|
|
94
|
+
{showQuestions ? (
|
|
95
|
+
<ChevronUp size={12} color={colors.secondaryText} />
|
|
96
|
+
) : (
|
|
97
|
+
<ChevronDown size={12} color={colors.secondaryText} />
|
|
98
|
+
)}
|
|
99
|
+
</Pressable>
|
|
100
|
+
{showQuestions && (
|
|
101
|
+
<View className="gap-1 pl-4">
|
|
102
|
+
{progress.subQuestions.map((q, i) => (
|
|
103
|
+
<Text key={i} className="text-xs text-muted-foreground">
|
|
104
|
+
{i + 1}. {q}
|
|
105
|
+
</Text>
|
|
106
|
+
))}
|
|
107
|
+
</View>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Current search query indicator */}
|
|
113
|
+
{progress.currentQuery && !isComplete && (
|
|
114
|
+
<View className="flex-row items-center gap-2 bg-muted/50 rounded-lg px-3 py-1.5">
|
|
115
|
+
<Loader2 size={12} color={colors.secondaryText} />
|
|
116
|
+
<Text className="text-xs text-muted-foreground flex-1" numberOfLines={1}>
|
|
117
|
+
{progress.currentQuery}
|
|
118
|
+
</Text>
|
|
119
|
+
</View>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Sources list (collapsed by default) */}
|
|
123
|
+
{progress.sources && progress.sources.length > 0 && (
|
|
124
|
+
<View>
|
|
125
|
+
<Pressable
|
|
126
|
+
onPress={() => setShowSources(!showSources)}
|
|
127
|
+
className="flex-row items-center gap-1"
|
|
128
|
+
>
|
|
129
|
+
<ExternalLink size={12} color={colors.secondaryText} />
|
|
130
|
+
<Text className="text-xs font-medium text-muted-foreground">
|
|
131
|
+
Sources ({progress.sources.length})
|
|
132
|
+
</Text>
|
|
133
|
+
{showSources ? (
|
|
134
|
+
<ChevronUp size={12} color={colors.secondaryText} />
|
|
135
|
+
) : (
|
|
136
|
+
<ChevronDown size={12} color={colors.secondaryText} />
|
|
137
|
+
)}
|
|
138
|
+
</Pressable>
|
|
139
|
+
{showSources && (
|
|
140
|
+
<View className="gap-1 pl-4 mt-1">
|
|
141
|
+
{progress.sources.map((s) => (
|
|
142
|
+
<Text key={s.id} className="text-xs text-primary" numberOfLines={1}>
|
|
143
|
+
[{s.id}] {s.title}
|
|
144
|
+
</Text>
|
|
145
|
+
))}
|
|
146
|
+
</View>
|
|
147
|
+
)}
|
|
148
|
+
</View>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Search count */}
|
|
152
|
+
{progress.totalSearches !== undefined && isComplete && (
|
|
153
|
+
<Text className="text-[10px] text-muted-foreground text-right">
|
|
154
|
+
{progress.totalSearches} searches performed
|
|
155
|
+
</Text>
|
|
156
|
+
)}
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
}
|
package/src/hooks/useAliaChat.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import { useOxy } from '@oxyhq/services';
|
|
3
3
|
import type { ChatMessage, ToolInvocation } from '../types';
|
|
4
|
+
import { getTextFromContent } from '../lib/content-utils';
|
|
4
5
|
|
|
5
6
|
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
6
7
|
|
|
@@ -188,7 +189,7 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
|
|
|
188
189
|
// Include full conversation history with tool context
|
|
189
190
|
for (const msg of messagesRef.current) {
|
|
190
191
|
if (msg.role === 'system') continue;
|
|
191
|
-
apiMessages.push({ role: msg.role, content: msg.content });
|
|
192
|
+
apiMessages.push({ role: msg.role, content: getTextFromContent(msg.content) });
|
|
192
193
|
// Include tool results so the AI has full context
|
|
193
194
|
if (msg.toolInvocations?.length) {
|
|
194
195
|
for (const tool of msg.toolInvocations) {
|
|
@@ -60,18 +60,32 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
60
60
|
return oxyServices.httpService.getAccessToken();
|
|
61
61
|
}, [options.accessToken, oxyServices]);
|
|
62
62
|
|
|
63
|
-
// Push metering to store while recording
|
|
63
|
+
// Push metering to store while recording (with epsilon guard to avoid thrashing subscribers)
|
|
64
|
+
const lastMeteringRef = useRef(0);
|
|
64
65
|
useEffect(() => {
|
|
65
|
-
if (state === 'recording'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
if (state === 'recording') {
|
|
67
|
+
let target: number;
|
|
68
|
+
if (recorderState.metering != null) {
|
|
69
|
+
// dBFS: -160 (silence) to 0 (max). Normalize using -60 as practical floor.
|
|
70
|
+
target = Math.min(1, Math.max(0, (recorderState.metering + 60) / 60));
|
|
71
|
+
} else {
|
|
72
|
+
// Metering unavailable — simulate gentle activity
|
|
73
|
+
target = 0.12 + Math.random() * 0.18;
|
|
74
|
+
}
|
|
75
|
+
if (Math.abs(target - lastMeteringRef.current) >= 0.02) {
|
|
76
|
+
lastMeteringRef.current = target;
|
|
77
|
+
sttStore.getState().setMetering(target);
|
|
78
|
+
}
|
|
69
79
|
}
|
|
70
|
-
}, [state, recorderState.metering]);
|
|
80
|
+
}, [state, recorderState.metering, recorderState.durationMillis]);
|
|
71
81
|
|
|
72
|
-
// Sync recording state to store
|
|
82
|
+
// Sync recording state to store + reset metering on stop
|
|
73
83
|
useEffect(() => {
|
|
74
84
|
sttStore.getState().setRecording(state === 'recording');
|
|
85
|
+
if (state !== 'recording') {
|
|
86
|
+
lastMeteringRef.current = 0;
|
|
87
|
+
sttStore.getState().setMetering(0);
|
|
88
|
+
}
|
|
75
89
|
}, [state]);
|
|
76
90
|
|
|
77
91
|
const startRecording = useCallback(async () => {
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type { AliaExpression, AliaFaceProps } from './components/AliaFace';
|
|
|
14
14
|
export { ThinkingIndicator } from './components/ThinkingIndicator';
|
|
15
15
|
export { AliaMarkdown } from './components/Markdown';
|
|
16
16
|
export { Reasoning, ReasoningTrigger, ReasoningContent } from './components/Reasoning';
|
|
17
|
+
export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
|
|
18
|
+
export { ResearchProgressCard } from './components/ResearchProgressCard';
|
|
19
|
+
export { PlanPreviewCard } from './components/PlanPreviewCard';
|
|
17
20
|
|
|
18
21
|
// ── Voice components ──
|
|
19
22
|
export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
|
|
@@ -39,9 +42,11 @@ export type { SoundName, SoundSources } from './hooks/useSoundEffects';
|
|
|
39
42
|
export type {
|
|
40
43
|
ChatMessage,
|
|
41
44
|
ToolInvocation,
|
|
42
|
-
|
|
45
|
+
WelcomeSuggestion,
|
|
43
46
|
ResearchProgress,
|
|
47
|
+
ResearchSource,
|
|
44
48
|
PendingPlan,
|
|
49
|
+
PlanStep,
|
|
45
50
|
RoomState,
|
|
46
51
|
AgentState,
|
|
47
52
|
VoiceMessage,
|
|
@@ -54,6 +59,8 @@ export type { ButtonProps } from './components/ui/button';
|
|
|
54
59
|
export { Text, TextClassContext } from './components/ui/text';
|
|
55
60
|
export { ChatTextInput } from './components/ui/chat-text-input';
|
|
56
61
|
export { cn, formatFileSize } from './lib/utils';
|
|
62
|
+
export { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from './lib/tool-registry';
|
|
63
|
+
export { getTextFromContent, getImagesFromContent } from './lib/content-utils';
|
|
57
64
|
|
|
58
65
|
// ── PromptInput ──
|
|
59
66
|
export { PromptInput } from './components/ui/prompt-input/prompt-input';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract text from message content, whether it's a string or multi-part array.
|
|
3
|
+
*/
|
|
4
|
+
export function getTextFromContent(content: string | Array<{ type: string; [key: string]: any }> | any): string {
|
|
5
|
+
if (typeof content === 'string') return content;
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content
|
|
8
|
+
.filter((p: any) => p.type === 'text')
|
|
9
|
+
.map((p: any) => p.text || '')
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
return String(content || '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract image URLs from multi-part message content.
|
|
17
|
+
*/
|
|
18
|
+
export function getImagesFromContent(content: string | Array<{ type: string; [key: string]: any }> | any): string[] {
|
|
19
|
+
if (!Array.isArray(content)) return [];
|
|
20
|
+
return content
|
|
21
|
+
.filter((p: any) => p.type === 'image_url' && p.image_url?.url)
|
|
22
|
+
.map((p: any) => p.image_url.url);
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface ToolDefinition {
|
|
2
|
+
label: string;
|
|
3
|
+
category: 'search' | 'communication' | 'utility' | 'memory';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const TOOL_REGISTRY: Record<string, ToolDefinition> = {
|
|
7
|
+
// Search
|
|
8
|
+
webSearch: { label: 'Searching the web', category: 'search' },
|
|
9
|
+
scrapeURL: { label: 'Reading URL', category: 'search' },
|
|
10
|
+
getTimeline: { label: 'Getting timeline', category: 'search' },
|
|
11
|
+
searchKnowledgeBase: { label: 'Searching knowledge base', category: 'search' },
|
|
12
|
+
webScraper: { label: 'Reading web page', category: 'search' },
|
|
13
|
+
browse: { label: 'Browsing the web', category: 'search' },
|
|
14
|
+
|
|
15
|
+
// Communication
|
|
16
|
+
sendWhatsAppMessage: { label: 'Sending WhatsApp message', category: 'communication' },
|
|
17
|
+
getWhatsAppChats: { label: 'Loading WhatsApp chats', category: 'communication' },
|
|
18
|
+
getWhatsAppMessages: { label: 'Reading WhatsApp messages', category: 'communication' },
|
|
19
|
+
sendTelegram: { label: 'Sending Telegram message', category: 'communication' },
|
|
20
|
+
|
|
21
|
+
// Utility
|
|
22
|
+
getCurrentDate: { label: 'Getting current date', category: 'utility' },
|
|
23
|
+
generateFile: { label: 'Generating file', category: 'utility' },
|
|
24
|
+
|
|
25
|
+
// Memory
|
|
26
|
+
saveUserMemory: { label: 'Saving to memory', category: 'memory' },
|
|
27
|
+
updateUserPreferences: { label: 'Updating preferences', category: 'memory' },
|
|
28
|
+
updateUserContext: { label: 'Updating context', category: 'memory' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function getToolLabel(toolName: string): string {
|
|
32
|
+
return TOOL_REGISTRY[toolName]?.label || toolName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getToolActiveLabel(toolName: string): string | undefined {
|
|
36
|
+
const label = TOOL_REGISTRY[toolName]?.label;
|
|
37
|
+
if (!label) return undefined;
|
|
38
|
+
return label + '...';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Status strings for the ThinkingIndicator during research phases. */
|
|
42
|
+
const RESEARCH_ACTIVE_LABELS: Record<string, string> = {
|
|
43
|
+
decomposing: 'Decomposing query...',
|
|
44
|
+
searching: 'Searching sources...',
|
|
45
|
+
reading: 'Reading articles...',
|
|
46
|
+
synthesizing: 'Synthesizing findings...',
|
|
47
|
+
follow_up: 'Following up...',
|
|
48
|
+
finalizing: 'Finalizing research...',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function getResearchActiveLabel(phase: string): string | undefined {
|
|
52
|
+
return RESEARCH_ACTIVE_LABELS[phase];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { TOOL_REGISTRY };
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
// ── Welcome / Suggestion Types ──
|
|
2
|
+
|
|
3
|
+
export interface WelcomeSuggestion {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
// ── Chat Message Types ──
|
|
2
10
|
|
|
11
|
+
export interface ResearchSource {
|
|
12
|
+
id: number;
|
|
13
|
+
url: string;
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
export interface ResearchProgress {
|
|
4
18
|
phase?: string;
|
|
5
19
|
message?: string;
|
|
@@ -7,6 +21,9 @@ export interface ResearchProgress {
|
|
|
7
21
|
sourcesFound?: number;
|
|
8
22
|
currentQuery?: string;
|
|
9
23
|
iteration?: number;
|
|
24
|
+
isComplete?: boolean;
|
|
25
|
+
sources?: ResearchSource[];
|
|
26
|
+
totalSearches?: number;
|
|
10
27
|
}
|
|
11
28
|
|
|
12
29
|
export interface PlanStep {
|
|
@@ -25,7 +42,7 @@ export interface PendingPlan {
|
|
|
25
42
|
export interface ChatMessage {
|
|
26
43
|
id: string;
|
|
27
44
|
role: 'user' | 'assistant' | 'system';
|
|
28
|
-
content: string;
|
|
45
|
+
content: string | Array<{ type: string; [key: string]: any }>;
|
|
29
46
|
toolInvocations?: ToolInvocation[];
|
|
30
47
|
createdAt: number;
|
|
31
48
|
/** Extended thinking / reasoning content */
|
|
@@ -42,6 +59,8 @@ export interface ChatMessage {
|
|
|
42
59
|
isStreaming?: boolean;
|
|
43
60
|
/** Cached TTS audio URL for read-aloud */
|
|
44
61
|
audioUrl?: string;
|
|
62
|
+
/** Delegated agent identity */
|
|
63
|
+
agentInfo?: { id: string; name: string; avatar: string | null };
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
export interface ToolInvocation {
|
|
@@ -53,12 +72,6 @@ export interface ToolInvocation {
|
|
|
53
72
|
toolCallId?: string;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
export interface AliaChatSuggestion {
|
|
57
|
-
label: string;
|
|
58
|
-
icon?: string;
|
|
59
|
-
prompt: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
75
|
// ── Voice Types ──
|
|
63
76
|
|
|
64
77
|
export type RoomState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
3
|
-
import { useAliaColors } from '../theme';
|
|
4
|
-
import type { AliaChatSuggestion } from '../types';
|
|
5
|
-
|
|
6
|
-
interface AliaChatSuggestionsProps {
|
|
7
|
-
suggestions: AliaChatSuggestion[];
|
|
8
|
-
onSelect: (prompt: string) => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function AliaChatSuggestions({ suggestions, onSelect }: AliaChatSuggestionsProps) {
|
|
12
|
-
const colors = useAliaColors();
|
|
13
|
-
const isDark = colors.isDark;
|
|
14
|
-
|
|
15
|
-
if (suggestions.length === 0) return null;
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<View style={styles.container}>
|
|
19
|
-
<Text style={[styles.greeting, { color: colors.tint }]}>
|
|
20
|
-
How can I help you today?
|
|
21
|
-
</Text>
|
|
22
|
-
<View style={styles.list}>
|
|
23
|
-
{suggestions.map((suggestion, i) => (
|
|
24
|
-
<TouchableOpacity
|
|
25
|
-
key={i}
|
|
26
|
-
style={[
|
|
27
|
-
styles.chip,
|
|
28
|
-
{ backgroundColor: isDark ? colors.card : colors.card },
|
|
29
|
-
]}
|
|
30
|
-
onPress={() => onSelect(suggestion.prompt)}
|
|
31
|
-
activeOpacity={0.7}
|
|
32
|
-
>
|
|
33
|
-
<Text style={[styles.chipText, { color: colors.text }]}>
|
|
34
|
-
{suggestion.label}
|
|
35
|
-
</Text>
|
|
36
|
-
</TouchableOpacity>
|
|
37
|
-
))}
|
|
38
|
-
</View>
|
|
39
|
-
</View>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const styles = StyleSheet.create({
|
|
44
|
-
container: {
|
|
45
|
-
paddingTop: 16,
|
|
46
|
-
paddingHorizontal: 16,
|
|
47
|
-
},
|
|
48
|
-
greeting: {
|
|
49
|
-
fontSize: 24,
|
|
50
|
-
fontWeight: '600',
|
|
51
|
-
lineHeight: 32,
|
|
52
|
-
marginBottom: 20,
|
|
53
|
-
paddingHorizontal: 4,
|
|
54
|
-
},
|
|
55
|
-
list: {
|
|
56
|
-
gap: 8,
|
|
57
|
-
},
|
|
58
|
-
chip: {
|
|
59
|
-
flexDirection: 'row',
|
|
60
|
-
alignItems: 'center',
|
|
61
|
-
gap: 12,
|
|
62
|
-
paddingHorizontal: 16,
|
|
63
|
-
paddingVertical: 14,
|
|
64
|
-
borderRadius: 16,
|
|
65
|
-
},
|
|
66
|
-
chipText: {
|
|
67
|
-
fontSize: 14,
|
|
68
|
-
flex: 1,
|
|
69
|
-
},
|
|
70
|
-
});
|