@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.
- package/package.json +25 -3
- package/src/components/AliaChatContent.tsx +356 -0
- package/src/components/AliaChatInput.tsx +173 -26
- package/src/components/AliaChatMessageList.tsx +459 -168
- package/src/components/AliaChatScreen.tsx +140 -0
- package/src/components/AliaChatSheet.tsx +41 -44
- 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/components/ui/button.tsx +104 -0
- package/src/components/ui/chat-text-input.tsx +164 -0
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/dropdown-menu.web.tsx +355 -0
- package/src/components/ui/prompt-input/actions.tsx +28 -0
- package/src/components/ui/prompt-input/add-menu.tsx +83 -0
- package/src/components/ui/prompt-input/attachments.tsx +183 -0
- package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
- package/src/components/ui/prompt-input/context.tsx +71 -0
- package/src/components/ui/prompt-input/mic-button.tsx +53 -0
- package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
- package/src/components/ui/prompt-input/submit-button.tsx +49 -0
- package/src/components/ui/prompt-input/textarea.tsx +64 -0
- package/src/components/ui/text.tsx +29 -0
- package/src/hooks/useAliaChat.ts +18 -8
- package/src/hooks/useDocumentPicker.ts +37 -0
- package/src/hooks/useImagePicker.ts +38 -0
- package/src/hooks/useSpeechToText.ts +25 -9
- package/src/index.ts +41 -3
- package/src/lib/content-utils.ts +23 -0
- package/src/lib/keyboard.native.tsx +6 -0
- package/src/lib/keyboard.tsx +25 -0
- package/src/lib/tool-registry.ts +55 -0
- package/src/lib/utils.ts +14 -0
- package/src/optional-deps.d.ts +17 -0
- package/src/types.ts +27 -10
- package/src/components/AliaChatSuggestions.tsx +0 -70
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { View, Pressable } from 'react-native';
|
|
2
|
+
import { Text } from './ui/text';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { Check, X, ListChecks } from 'lucide-react-native';
|
|
5
|
+
import Animated, { FadeInUp } from 'react-native-reanimated';
|
|
6
|
+
import type { PlanStep } from '../types';
|
|
7
|
+
|
|
8
|
+
interface PlanPreviewCardProps {
|
|
9
|
+
steps: PlanStep[];
|
|
10
|
+
onApprove: () => void;
|
|
11
|
+
onReject: () => void;
|
|
12
|
+
approved?: boolean;
|
|
13
|
+
rejected?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PlanPreviewCard({ steps, onApprove, onReject, approved, rejected }: PlanPreviewCardProps) {
|
|
17
|
+
if (approved) {
|
|
18
|
+
return (
|
|
19
|
+
<View className="flex-row items-center gap-1.5 py-1">
|
|
20
|
+
<Check size={12} className="text-green-500" />
|
|
21
|
+
<Text className="text-xs text-muted-foreground">Plan approved</Text>
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (rejected) {
|
|
27
|
+
return (
|
|
28
|
+
<View className="flex-row items-center gap-1.5 py-1">
|
|
29
|
+
<X size={12} className="text-red-500" />
|
|
30
|
+
<Text className="text-xs text-muted-foreground">Plan cancelled</Text>
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Animated.View entering={FadeInUp.springify()}>
|
|
37
|
+
<View className="border border-border rounded-xl bg-card p-4 gap-3 my-2">
|
|
38
|
+
<View className="flex-row items-center gap-2">
|
|
39
|
+
<ListChecks size={16} className="text-foreground" />
|
|
40
|
+
<Text className="text-sm font-semibold text-foreground">Here's my plan</Text>
|
|
41
|
+
</View>
|
|
42
|
+
|
|
43
|
+
<View className="gap-2">
|
|
44
|
+
{steps.map((step, i) => (
|
|
45
|
+
<View key={i} className="flex-row items-start gap-2.5">
|
|
46
|
+
<View className="w-5 h-5 rounded-full bg-muted items-center justify-center mt-0.5">
|
|
47
|
+
<Text className="text-[10px] font-bold text-muted-foreground">{i + 1}</Text>
|
|
48
|
+
</View>
|
|
49
|
+
<View className="flex-1">
|
|
50
|
+
<Text className="text-[13px] font-medium text-foreground">{step.action}</Text>
|
|
51
|
+
{step.description ? (
|
|
52
|
+
<Text className="text-xs text-muted-foreground">{step.description}</Text>
|
|
53
|
+
) : null}
|
|
54
|
+
</View>
|
|
55
|
+
</View>
|
|
56
|
+
))}
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
<View className="flex-row gap-2 pt-1">
|
|
60
|
+
<Button onPress={onApprove} size="sm" className="flex-1">
|
|
61
|
+
<View className="flex-row items-center gap-1.5">
|
|
62
|
+
<Check size={14} className="text-primary-foreground" />
|
|
63
|
+
<Text className="text-sm font-medium text-primary-foreground">Approve</Text>
|
|
64
|
+
</View>
|
|
65
|
+
</Button>
|
|
66
|
+
<Pressable
|
|
67
|
+
onPress={onReject}
|
|
68
|
+
className="flex-1 items-center justify-center py-2 rounded-lg active:opacity-70"
|
|
69
|
+
>
|
|
70
|
+
<Text className="text-sm text-muted-foreground">Cancel</Text>
|
|
71
|
+
</Pressable>
|
|
72
|
+
</View>
|
|
73
|
+
</View>
|
|
74
|
+
</Animated.View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Pressable, ActivityIndicator } from "react-native";
|
|
4
|
+
import { TextClassContext } from "./text";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"group flex items-center justify-center rounded-xl web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary web:hover:opacity-90 active:opacity-90",
|
|
13
|
+
destructive: "bg-destructive web:hover:opacity-90 active:opacity-90",
|
|
14
|
+
outline:
|
|
15
|
+
"border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
|
16
|
+
secondary: "bg-secondary web:hover:opacity-80 active:opacity-80",
|
|
17
|
+
ghost:
|
|
18
|
+
"web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
|
19
|
+
link: "web:underline-offset-4 web:hover:underline web:focus:underline ",
|
|
20
|
+
pill: "text-primary-foreground rounded-full bg-black web:hover:opacity-90 active:opacity-90",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-9 px-3.5 py-2 native:h-11 native:px-4",
|
|
24
|
+
sm: "h-8 rounded-lg px-2.5",
|
|
25
|
+
lg: "h-10 rounded-xl px-6 native:h-12",
|
|
26
|
+
icon: "h-9 w-9",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const buttonTextVariants = cva(
|
|
37
|
+
"web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors",
|
|
38
|
+
{
|
|
39
|
+
variants: {
|
|
40
|
+
variant: {
|
|
41
|
+
default: "text-primary-foreground",
|
|
42
|
+
destructive: "text-destructive-foreground",
|
|
43
|
+
outline: "group-active:text-accent-foreground",
|
|
44
|
+
secondary:
|
|
45
|
+
"text-secondary-foreground group-active:text-secondary-foreground",
|
|
46
|
+
ghost: "group-active:text-accent-foreground",
|
|
47
|
+
link: "text-primary group-active:underline",
|
|
48
|
+
pill: "text-primary-foreground",
|
|
49
|
+
},
|
|
50
|
+
size: {
|
|
51
|
+
default: "",
|
|
52
|
+
sm: "",
|
|
53
|
+
lg: "native:text-lg",
|
|
54
|
+
icon: "",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: {
|
|
58
|
+
variant: "default",
|
|
59
|
+
size: "default",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> &
|
|
65
|
+
VariantProps<typeof buttonVariants> & {
|
|
66
|
+
isLoading?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const Button = React.forwardRef<
|
|
70
|
+
React.ElementRef<typeof Pressable>,
|
|
71
|
+
ButtonProps
|
|
72
|
+
>(({ className, variant, size, isLoading, children, ...props }, ref) => {
|
|
73
|
+
return (
|
|
74
|
+
<TextClassContext.Provider
|
|
75
|
+
value={cn(
|
|
76
|
+
props.disabled && "web:pointer-events-none",
|
|
77
|
+
buttonTextVariants({ variant, size }),
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<Pressable
|
|
81
|
+
className={cn(
|
|
82
|
+
props.disabled && "opacity-50 web:pointer-events-none",
|
|
83
|
+
isLoading && "web:pointer-events-none",
|
|
84
|
+
buttonVariants({ variant, size, className }),
|
|
85
|
+
)}
|
|
86
|
+
ref={ref}
|
|
87
|
+
role="button"
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{isLoading ? (
|
|
91
|
+
<ActivityIndicator
|
|
92
|
+
color={variant === "outline" ? "black" : "white"}
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
children
|
|
96
|
+
)}
|
|
97
|
+
</Pressable>
|
|
98
|
+
</TextClassContext.Provider>
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
Button.displayName = "Button";
|
|
102
|
+
|
|
103
|
+
export { Button, buttonTextVariants, buttonVariants };
|
|
104
|
+
export type { ButtonProps };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
TextInput,
|
|
4
|
+
Platform,
|
|
5
|
+
View,
|
|
6
|
+
type NativeSyntheticEvent,
|
|
7
|
+
type TextInputKeyPressEventData,
|
|
8
|
+
type NativeSyntheticEvent as RNSyntheticEvent,
|
|
9
|
+
type TextInputContentSizeChangeEventData,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { cn } from "../../lib/utils";
|
|
12
|
+
|
|
13
|
+
type ChatTextInputProps = React.ComponentPropsWithoutRef<typeof TextInput> & {
|
|
14
|
+
noFocus?: boolean;
|
|
15
|
+
onEnterPress?: () => void;
|
|
16
|
+
onCompletionKey?: (key: string) => boolean;
|
|
17
|
+
maxHeight?: number;
|
|
18
|
+
minHeight?: number;
|
|
19
|
+
onHeightChange?: (height: number) => void;
|
|
20
|
+
disableEnterToSubmit?: boolean;
|
|
21
|
+
disableAutoHeight?: boolean;
|
|
22
|
+
onImagePaste?: (files: File[]) => void;
|
|
23
|
+
fillContainer?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
27
|
+
({
|
|
28
|
+
className,
|
|
29
|
+
noFocus = false,
|
|
30
|
+
onEnterPress,
|
|
31
|
+
onCompletionKey,
|
|
32
|
+
onKeyPress,
|
|
33
|
+
maxHeight = 200,
|
|
34
|
+
minHeight = 44,
|
|
35
|
+
onContentSizeChange,
|
|
36
|
+
onHeightChange,
|
|
37
|
+
style,
|
|
38
|
+
disableEnterToSubmit = false,
|
|
39
|
+
disableAutoHeight = false,
|
|
40
|
+
onImagePaste,
|
|
41
|
+
fillContainer = false,
|
|
42
|
+
...props
|
|
43
|
+
}, ref) => {
|
|
44
|
+
const inputRef = React.useRef<TextInput>(null);
|
|
45
|
+
const wrapperRef = React.useRef<View>(null);
|
|
46
|
+
|
|
47
|
+
React.useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
48
|
+
|
|
49
|
+
// Attach paste event listener (web only)
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (Platform.OS !== 'web' || !onImagePaste) return;
|
|
52
|
+
|
|
53
|
+
const handlePaste = (e: Event) => {
|
|
54
|
+
const clipboardEvent = e as ClipboardEvent;
|
|
55
|
+
|
|
56
|
+
// @ts-ignore - web-specific API
|
|
57
|
+
const activeElement = document.activeElement;
|
|
58
|
+
// @ts-ignore - web-specific API
|
|
59
|
+
const wrapper = wrapperRef.current;
|
|
60
|
+
|
|
61
|
+
// @ts-ignore - web-specific API
|
|
62
|
+
const isContained = wrapper && wrapper.contains(activeElement);
|
|
63
|
+
|
|
64
|
+
if (!isContained) return;
|
|
65
|
+
|
|
66
|
+
const items = clipboardEvent.clipboardData?.items;
|
|
67
|
+
if (!items) return;
|
|
68
|
+
|
|
69
|
+
const imageFiles: File[] = [];
|
|
70
|
+
for (let i = 0; i < items.length; i++) {
|
|
71
|
+
const item = items[i];
|
|
72
|
+
if (item.type.indexOf('image') !== -1) {
|
|
73
|
+
const file = item.getAsFile();
|
|
74
|
+
if (file) {
|
|
75
|
+
imageFiles.push(file);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (imageFiles.length > 0) {
|
|
81
|
+
clipboardEvent.preventDefault();
|
|
82
|
+
onImagePaste(imageFiles);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// @ts-ignore - web-specific API
|
|
87
|
+
document.addEventListener('paste', handlePaste);
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
// @ts-ignore - web-specific API
|
|
91
|
+
document.removeEventListener('paste', handlePaste);
|
|
92
|
+
};
|
|
93
|
+
}, [onImagePaste]);
|
|
94
|
+
|
|
95
|
+
const handleKeyPress = (
|
|
96
|
+
e: NativeSyntheticEvent<TextInputKeyPressEventData>
|
|
97
|
+
) => {
|
|
98
|
+
onKeyPress?.(e);
|
|
99
|
+
|
|
100
|
+
const key = e.nativeEvent.key;
|
|
101
|
+
|
|
102
|
+
if (onCompletionKey && (key === "ArrowUp" || key === "ArrowDown" || key === "Enter" || key === "Escape")) {
|
|
103
|
+
if (onCompletionKey(key)) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (key === "Enter" && !disableEnterToSubmit) {
|
|
110
|
+
// @ts-ignore - shiftKey exists on web
|
|
111
|
+
if (Platform.OS !== 'web' || !e.nativeEvent.shiftKey) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
onEnterPress?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleContentSizeChange = (
|
|
119
|
+
e: RNSyntheticEvent<TextInputContentSizeChangeEventData>
|
|
120
|
+
) => {
|
|
121
|
+
onContentSizeChange?.(e);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<View
|
|
126
|
+
ref={wrapperRef}
|
|
127
|
+
style={{ width: '100%', ...(fillContainer && { flex: 1 }) }}
|
|
128
|
+
onLayout={(e) => onHeightChange?.(e.nativeEvent.layout.height)}
|
|
129
|
+
>
|
|
130
|
+
<TextInput
|
|
131
|
+
ref={inputRef}
|
|
132
|
+
className={cn(
|
|
133
|
+
"native:text-md native:leading-[1.25] rounded-xl border border-input bg-background px-3.5 text-base text-foreground file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground web:flex web:w-full web:py-2 lg:text-sm",
|
|
134
|
+
"web:ring-offset-background web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
|
135
|
+
!fillContainer && !props.multiline && "h-9",
|
|
136
|
+
fillContainer && "h-full",
|
|
137
|
+
noFocus && "web:focus-visible:ring-0 web:focus-visible:ring-offset-0",
|
|
138
|
+
props.editable === false && "opacity-50 web:cursor-not-allowed",
|
|
139
|
+
className
|
|
140
|
+
)}
|
|
141
|
+
placeholderClassName={cn("text-muted-foreground", props.placeholderClassName)}
|
|
142
|
+
onKeyPress={handleKeyPress}
|
|
143
|
+
onContentSizeChange={handleContentSizeChange}
|
|
144
|
+
scrollEnabled={fillContainer || props.multiline}
|
|
145
|
+
style={[
|
|
146
|
+
style,
|
|
147
|
+
!fillContainer && props.multiline && !disableAutoHeight && {
|
|
148
|
+
minHeight,
|
|
149
|
+
maxHeight,
|
|
150
|
+
overflow: 'auto' as any,
|
|
151
|
+
...(Platform.OS === 'web' ? { fieldSizing: 'content' } : {}),
|
|
152
|
+
},
|
|
153
|
+
fillContainer && { flex: 1, height: '100%' },
|
|
154
|
+
]}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
ChatTextInput.displayName = "ChatTextInput";
|
|
163
|
+
|
|
164
|
+
export { ChatTextInput };
|