vorqard-ai-sdk 1.0.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.
@@ -0,0 +1,86 @@
1
+ import React from 'react';
2
+ import { View, Text, TouchableOpacity, Image } from 'react-native';
3
+ import Ionicons from 'react-native-vector-icons/Ionicons';
4
+ import { styles } from '../ChatStyles';
5
+ import { getAbsoluteProfilePicture, getFormattedDoctorName } from '../ChatUtils';
6
+
7
+ // 6. Slot Selection picker
8
+ export const SlotSelectionCard = ({ doctor, selectedDate, options, onOptionPress }) => {
9
+ const days = [];
10
+ const startDay = new Date();
11
+ for (let i = 0; i < 5; i++) {
12
+ const d = new Date(startDay);
13
+ d.setDate(startDay.getDate() + i);
14
+ const dateStr = d.toISOString().split('T')[0];
15
+ const dayName = d.toLocaleDateString('en-US', { weekday: 'short' });
16
+ const dayNum = d.getDate();
17
+ days.push({ dateStr, dayName, dayNum });
18
+ }
19
+
20
+ return (
21
+ <View style={styles.slotPickerCard}>
22
+ {doctor && (
23
+ <View style={styles.miniDocRow}>
24
+ {(() => {
25
+ const picUri = getAbsoluteProfilePicture(doctor.profile_picture);
26
+ return picUri ? (
27
+ <Image source={{ uri: picUri }} style={styles.miniDocAvatar} />
28
+ ) : (
29
+ <View style={[styles.miniDocAvatar, { backgroundColor: '#FFFFFF', borderWidth: 1, borderColor: '#E2E8F0', justifyContent: 'center', alignItems: 'center' }]}>
30
+ <Ionicons name="person" size={16} color="#CBD5E1" />
31
+ </View>
32
+ );
33
+ })()}
34
+ <View style={{ flex: 1, marginLeft: 10 }}>
35
+ <Text style={styles.miniDocName}>{getFormattedDoctorName(doctor.name)}</Text>
36
+ <Text style={styles.miniDocSpecialty}>{doctor.specialty}</Text>
37
+ </View>
38
+ <View style={styles.miniDocRating}>
39
+ <Ionicons name="star" size={12} color="#F59E0B" />
40
+ <Text style={styles.miniDocRatingVal}>{doctor.rating || "0.0"}</Text>
41
+ </View>
42
+ </View>
43
+ )}
44
+
45
+ <Text style={styles.pickerHeading}>Select Date</Text>
46
+ <View style={styles.datePickerRow}>
47
+ {days.map((day) => {
48
+ const isSelected = day.dateStr === selectedDate;
49
+ return (
50
+ <TouchableOpacity
51
+ key={day.dateStr}
52
+ style={[styles.dateItem, isSelected && styles.dateItemActive]}
53
+ onPress={() => onOptionPress?.({ id: `date_${day.dateStr}`, label: `date:${day.dateStr}` })}
54
+ activeOpacity={0.8}
55
+ >
56
+ <Text style={[styles.dayNameText, isSelected && styles.dayNameTextActive]}>
57
+ {day.dayName}
58
+ </Text>
59
+ <Text style={[styles.dayNumText, isSelected && styles.dayNumTextActive]}>
60
+ {day.dayNum}
61
+ </Text>
62
+ </TouchableOpacity>
63
+ );
64
+ })}
65
+ </View>
66
+
67
+ <Text style={styles.pickerHeading}>Available Time Slots</Text>
68
+ {options.length === 0 ? (
69
+ <Text style={styles.noSlotsText}>No slots available for this date.</Text>
70
+ ) : (
71
+ <View style={styles.slotsGrid}>
72
+ {options.map((opt) => (
73
+ <TouchableOpacity
74
+ key={opt.id}
75
+ style={styles.slotButton}
76
+ onPress={() => onOptionPress?.(opt)}
77
+ activeOpacity={0.7}
78
+ >
79
+ <Text style={styles.slotBtnText}>{opt.label}</Text>
80
+ </TouchableOpacity>
81
+ ))}
82
+ </View>
83
+ )}
84
+ </View>
85
+ );
86
+ };
@@ -0,0 +1,145 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { getSDKClient } from '../api/sdkClient';
3
+
4
+ // ─── Module-level in-memory store ─────────────────────────────────────────────
5
+ // Persists chat history for the entire app session (survives screen nav).
6
+ // Keyed by patientName so different users don't share history.
7
+ const _chatStore = {};
8
+
9
+ const applySafetyRules = (text) => {
10
+ const lower = text.toLowerCase();
11
+ const emergencyWords = ['chest pain', 'breathing difficulty', 'severe bleeding', 'lose consciousness'];
12
+ if (emergencyWords.some(w => lower.includes(w))) {
13
+ return 'Please call emergency services immediately (112 in India). This is not a medical emergency hotline.\n\n' + text;
14
+ }
15
+ return text;
16
+ };
17
+
18
+ const formatDate = (isoString) => {
19
+ if (!isoString) return null;
20
+ try {
21
+ const date = new Date(isoString);
22
+ return date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
23
+ } catch {
24
+ return isoString.split('T')[0];
25
+ }
26
+ };
27
+
28
+ export const useHealthChat = (patientName = '') => {
29
+ const storeKey = patientName || '__default__';
30
+
31
+ const [messages, setMessagesState] = useState(() => _chatStore[storeKey] || []);
32
+ const [inputText, setInputText] = useState('');
33
+ const [loading, setLoading] = useState(false);
34
+ const [patientContext, setPatientContext] = useState(null);
35
+ const flatListRef = useRef(null);
36
+
37
+ // Keep the module-level store in sync whenever messages change
38
+ const setMessages = (updater) => {
39
+ setMessagesState(prev => {
40
+ const next = typeof updater === 'function' ? updater(prev) : updater;
41
+ _chatStore[storeKey] = next;
42
+ return next;
43
+ });
44
+ };
45
+
46
+ const clearChat = () => {
47
+ _chatStore[storeKey] = [];
48
+ setMessagesState([]);
49
+ setInputText('');
50
+ };
51
+
52
+ const loadChatContext = async () => {
53
+ try {
54
+ const [dashRes, recordsRes] = await Promise.all([
55
+ getSDKClient().get('/patients/dashboard/me'),
56
+ getSDKClient().get('/patients/records/me')
57
+ ]);
58
+
59
+ const consultations = (recordsRes.data || [])
60
+ .filter(r => r.type === 'consultation')
61
+ .sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
62
+
63
+ const lastVisit = consultations[0];
64
+ const diagnosis = lastVisit?.diagnosis || 'General Checkup';
65
+ const rawDate = lastVisit?.created_at || null;
66
+ const formattedDate = rawDate ? formatDate(rawDate) : 'Recently';
67
+
68
+ setPatientContext({
69
+ diagnosis,
70
+ date: formattedDate,
71
+ status: 'Completed',
72
+ });
73
+ } catch (err) {
74
+ console.log('[SDK useHealthChat] Failed to load chat context:', err);
75
+ setPatientContext({
76
+ diagnosis: 'General Checkup',
77
+ date: 'Recently',
78
+ status: 'Completed',
79
+ });
80
+ }
81
+ };
82
+
83
+ const sendMessage = async (text = inputText) => {
84
+ if (!text.trim() || loading) return;
85
+
86
+ const isSystemAction = text.startsWith("date:");
87
+ if (!isSystemAction) {
88
+ const userMsg = { role: 'user', content: text, timestamp: new Date().toISOString() };
89
+ setMessages(prev => [...prev, userMsg]);
90
+ }
91
+
92
+ setInputText('');
93
+ setLoading(true);
94
+ try {
95
+ const history = (_chatStore[storeKey] || []).map(m => ({ role: m.role, content: m.content }));
96
+ if (!isSystemAction) history[history.length - 1] = { role: 'user', content: text };
97
+
98
+ const res = await getSDKClient().post('/patients/ai/chat', { message: text, history });
99
+
100
+ if (isSystemAction) {
101
+ setMessages(prev => {
102
+ const next = [...prev];
103
+ for (let i = next.length - 1; i >= 0; i--) {
104
+ if (next[i].role === 'assistant') {
105
+ next[i] = {
106
+ ...next[i],
107
+ content: applySafetyRules(res.data.response),
108
+ ui_action: res.data.ui_action || null,
109
+ };
110
+ break;
111
+ }
112
+ }
113
+ return next;
114
+ });
115
+ } else {
116
+ setMessages(prev => [...prev, {
117
+ role: 'assistant',
118
+ content: applySafetyRules(res.data.response),
119
+ ui_action: res.data.ui_action || null,
120
+ timestamp: new Date().toISOString(),
121
+ }]);
122
+ }
123
+ } catch {
124
+ setMessages(prev => [...prev, {
125
+ role: 'assistant',
126
+ content: "I'm having trouble connecting to my knowledge base. Please try again in a moment.",
127
+ timestamp: new Date().toISOString(),
128
+ }]);
129
+ } finally {
130
+ setLoading(false);
131
+ }
132
+ };
133
+
134
+ return {
135
+ messages,
136
+ inputText,
137
+ setInputText,
138
+ loading,
139
+ flatListRef,
140
+ loadChatContext,
141
+ sendMessage,
142
+ patientContext,
143
+ clearChat,
144
+ };
145
+ };
@@ -0,0 +1,93 @@
1
+ import { useState, useRef } from 'react';
2
+ import { Alert, Animated } from 'react-native';
3
+ import ImagePicker from 'react-native-image-crop-picker';
4
+ import { getSDKClient } from '../api/sdkClient';
5
+
6
+ export const useReportAnalysis = () => {
7
+ const [selectedImage, setSelectedImage] = useState(null);
8
+ const [loading, setLoading] = useState(false);
9
+ const [result, setResult] = useState(null);
10
+ const fadeAnim = useRef(new Animated.Value(0)).current;
11
+
12
+ const showResult = (data) => {
13
+ setResult(data);
14
+ Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start();
15
+ };
16
+
17
+ const pickFromGallery = async () => {
18
+ try {
19
+ const image = await ImagePicker.openPicker({
20
+ mediaType: 'photo',
21
+ includeBase64: true,
22
+ compressImageQuality: 0.8,
23
+ });
24
+ setSelectedImage({ uri: image.path, base64: image.data, mimeType: image.mime || 'image/jpeg' });
25
+ setResult(null);
26
+ fadeAnim.setValue(0);
27
+ } catch (error) {
28
+ if (error.code !== 'E_PICKER_CANCELLED') {
29
+ Alert.alert('Error', 'Could not open gallery or permission denied.');
30
+ }
31
+ }
32
+ };
33
+
34
+ const captureWithCamera = async () => {
35
+ try {
36
+ const image = await ImagePicker.openCamera({
37
+ mediaType: 'photo',
38
+ includeBase64: true,
39
+ compressImageQuality: 0.8,
40
+ });
41
+ setSelectedImage({ uri: image.path, base64: image.data, mimeType: image.mime || 'image/jpeg' });
42
+ setResult(null);
43
+ fadeAnim.setValue(0);
44
+ } catch (error) {
45
+ if (error.code !== 'E_PICKER_CANCELLED') {
46
+ Alert.alert('Error', 'Could not open camera or permission denied.');
47
+ }
48
+ }
49
+ };
50
+
51
+ const analyzeReport = async () => {
52
+ if (!selectedImage?.base64) {
53
+ Alert.alert('No Report', 'Please select or capture a report image first.');
54
+ return;
55
+ }
56
+ setLoading(true);
57
+ try {
58
+ const response = await getSDKClient().post('/patients/ai/analyze-report', {
59
+ image_base64: selectedImage.base64,
60
+ mime_type: selectedImage.mimeType,
61
+ });
62
+ showResult(response.data);
63
+ } catch (error) {
64
+ Alert.alert('Analysis Failed', 'Could not analyze the report. Please try again.');
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
69
+
70
+ const clearImage = () => {
71
+ setSelectedImage(null);
72
+ setResult(null);
73
+ };
74
+
75
+ const getScoreGradient = () => {
76
+ if (!result) return ['#2563EB', '#1D4ED8'];
77
+ if (result.health_score >= 80) return ['#10B981', '#059669'];
78
+ if (result.health_score >= 60) return ['#F59E0B', '#D97706'];
79
+ return ['#EF4444', '#DC2626'];
80
+ };
81
+
82
+ return {
83
+ selectedImage,
84
+ loading,
85
+ result,
86
+ fadeAnim,
87
+ pickFromGallery,
88
+ captureWithCamera,
89
+ analyzeReport,
90
+ clearImage,
91
+ getScoreGradient,
92
+ };
93
+ };
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ import React, { useEffect } from 'react';
2
+ import { initSDKClient } from './api/sdkClient';
3
+ import { AIHealthChatScreen } from './screens/AIHealthChatScreen';
4
+ import { AIReportAnalysisScreen } from './screens/AIReportAnalysisScreen';
5
+
6
+ export { initSDKClient, getSDKClient } from './api/sdkClient';
7
+ export { useHealthChat } from './hooks/useHealthChat';
8
+ export { AIReportAnalysisScreen };
9
+
10
+ export const VorqardAIAssistant = ({
11
+ backendUrl,
12
+ userToken,
13
+ patientName = 'there',
14
+ onClose,
15
+ aiLogo,
16
+ onAnalyzeReportPress
17
+ }) => {
18
+ if (backendUrl) {
19
+ initSDKClient(backendUrl, userToken);
20
+ }
21
+
22
+ return (
23
+ <AIHealthChatScreen
24
+ patientName={patientName}
25
+ onClose={onClose}
26
+ aiLogo={aiLogo}
27
+ onAnalyzeReportPress={onAnalyzeReportPress}
28
+ />
29
+ );
30
+ };