Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/models/interfaces/HelpAi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export type HelpChatMetadata = {
timezone?: string;
};

export type ScreenshotAsset = {
uri: string;
mimeType: string;
};

export type HelpDraft = {
kind: 'bug' | 'feature';
title: string;
Expand Down
8 changes: 6 additions & 2 deletions src/screens/HelpAi/HelpAiChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
HelpChatMetadata,
HelpChatResponse,
HelpEscalationCard,
ScreenshotAsset,
} from 'src/models/interfaces/HelpAi';
import Relay from 'src/services/backend/Relay';
import { useQuery } from '@realm/react';
Expand Down Expand Up @@ -165,6 +166,7 @@ const HelpAiChat = ({ navigation, route }) => {
const [typing, setTyping] = useState(false);
const [lastFailedText, setLastFailedText] = useState<string | null>(null);
const [showDisclaimerModal, setShowDisclaimerModal] = useState(false);
const lastScreenshotUrlsRef = useRef<ScreenshotAsset[]>([]);

useEffect(() => {
dispatch(createHelpAiThread({ conversationId }));
Expand Down Expand Up @@ -296,8 +298,9 @@ const HelpAiChat = ({ navigation, route }) => {
}
};

const submitDraftIssue = async () => {
const submitDraftIssue = async (screenshots: ScreenshotAsset[] = []) => {
if (!draft || !chatMeta) return;
lastScreenshotUrlsRef.current = screenshots;
if (!appId) {
showToast('Missing app id. Please restart the app and try again.');
return;
Expand All @@ -324,6 +327,7 @@ const HelpAiChat = ({ navigation, route }) => {
platform: chatMeta.platform,
device: chatMeta.device,
},
screenshots: screenshots.length > 0 ? screenshots : undefined,
});

dispatch(incrementHelpAiIssueCount({ conversationId }));
Expand Down Expand Up @@ -524,7 +528,7 @@ const HelpAiChat = ({ navigation, route }) => {
setHelpAiDraftStatus({ conversationId, draftStatus: 'pending_review' })
)
}
onRetry={submitDraftIssue}
onRetry={() => submitDraftIssue(lastScreenshotUrlsRef.current)}
/>
)}
</>
Expand Down
106 changes: 102 additions & 4 deletions src/screens/HelpAi/components/HelpAiDraftCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useColorMode } from '@gluestack-ui/themed-native-base';
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text, useColorScheme, View } from 'react-native';
import { ActivityIndicator, Image, Pressable, StyleSheet, Text, View } from 'react-native';
import { launchImageLibrary } from 'react-native-image-picker';
import Buttons from 'src/components/Buttons';
import { hp, wp } from 'src/constants/responsive';
import { HelpDraft } from 'src/models/interfaces/HelpAi';
import Colors from 'src/theme/Colors';
import { HelpDraft, ScreenshotAsset } from 'src/models/interfaces/HelpAi';

type DraftStatus =
| 'pending_review'
Expand All @@ -16,7 +18,7 @@ type HelpAiDraftCardProps = {
draft: HelpDraft;
draftStatus: DraftStatus;
onReview: () => void;
onConfirm: () => void;
onConfirm: (assets: ScreenshotAsset[]) => void;
onCancel: () => void;
onRetry: () => void;
};
Expand All @@ -38,6 +40,8 @@ const HelpAiDraftCard = ({
const t = setTimeout(() => forceUpdate((n) => n + 1), 100);
return () => clearTimeout(t);
}, [draftStatus]);

const [selectedAssets, setSelectedAssets] = useState<ScreenshotAsset[]>([]);
const uiColors = React.useMemo(
() => ({
surface: isDarkMode ? '#1f1f1f' : '#ffffff',
Expand All @@ -49,6 +53,25 @@ const HelpAiDraftCard = ({
[isDarkMode]
);

const handleAddScreenshot = () => {
launchImageLibrary(
{ mediaType: 'photo', maxWidth: 1200, maxHeight: 1200, quality: 0.7 },
(response) => {
if (response.didCancel || response.errorCode) return;
const asset = response.assets?.[0];
if (!asset?.uri) return;
setSelectedAssets((prev) => [
...prev,
{ uri: asset.uri, mimeType: asset.type || 'image/jpeg' },
]);
}
);
};

const handleConfirm = () => {
onConfirm(selectedAssets);
};

const fields: Array<{ label: string; value?: string | string[] }> = [
{ label: 'Title', value: draft.title },
...(draft.steps ? [{ label: 'Steps', value: draft.steps }] : []),
Expand Down Expand Up @@ -118,9 +141,46 @@ const HelpAiDraftCard = ({
This will create a public GitHub issue. Confirm only if you are comfortable sharing this
information publicly.
</Text>

{selectedAssets.length > 0 && (
<View style={styles.thumbnailStrip}>
{selectedAssets.map((asset, idx) => (
<View key={asset.uri} style={styles.thumbnailWrapper}>
<Image source={{ uri: asset.uri }} style={styles.thumbnail} />
<Pressable
testID={`screenshot_remove_${idx}`}
style={styles.thumbnailRemoveBtn}
onPress={() =>
setSelectedAssets((prev) => prev.filter((_, i) => i !== idx))
}
>
<Text style={styles.thumbnailRemoveText}>×</Text>
</Pressable>
</View>
))}
</View>
)}

{selectedAssets.length < 3 && (
<Pressable
testID="btn_add_screenshot"
style={[styles.addScreenshotRow, { borderColor: Colors.greyBorder }]}
onPress={handleAddScreenshot}
>
<Text style={{ fontSize: 12, color: uiColors.secondaryText }}>
+ Add Screenshot (optional, max 3)
</Text>
</Pressable>
)}

<Text style={{ fontSize: 12, color: uiColors.secondaryText }}>
Screenshots may expose wallet balances or addresses. Only attach what you are
comfortable sharing publicly.
</Text>

<Buttons
primaryText={'Confirm Public Submission'}
primaryCallback={onConfirm}
primaryCallback={handleConfirm}
fullWidth
/>
<Buttons primaryText={'Cancel'} primaryCallback={onCancel} fullWidth />
Expand Down Expand Up @@ -161,6 +221,44 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: wp(8),
},
thumbnailStrip: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: wp(8),
},
thumbnailWrapper: {
position: 'relative',
},
thumbnail: {
width: wp(80),
height: wp(80),
borderRadius: 8,
},
thumbnailRemoveBtn: {
position: 'absolute',
top: -6,
right: -6,
width: 20,
height: 20,
borderRadius: 100,
backgroundColor: '#555',
alignItems: 'center',
justifyContent: 'center',
},
thumbnailRemoveText: {
color: '#fff',
fontSize: 14,
lineHeight: 18,
fontWeight: '600',
},
addScreenshotRow: {
borderWidth: 1,
borderStyle: 'dashed',
borderRadius: 10,
paddingVertical: hp(10),
alignItems: 'center',
justifyContent: 'center',
},
});

export default HelpAiDraftCard;
22 changes: 21 additions & 1 deletion src/services/backend/Relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HelpChatResponse,
HelpDraft,
HelpIssueSubmitResponse,
ScreenshotAsset,
} from 'src/models/interfaces/HelpAi';
import axios, { AxiosResponse } from 'axios';
import { AverageTxFeesByNetwork } from 'src/services/wallets/interfaces';
Expand Down Expand Up @@ -749,9 +750,25 @@ export default class Relay {
idempotencyKey: string;
draft: HelpDraft;
metadata: Pick<HelpChatMetadata, 'appVersion' | 'platform' | 'device'>;
screenshots?: ScreenshotAsset[];
}): Promise<HelpIssueSubmitResponse> => {
try {
const res = await RestClient.post(`${RELAY}submitHelpIssue`, payload);
const { screenshots, ...rest } = payload;
const formData = new FormData();
formData.append('payload', JSON.stringify(rest));
if (screenshots) {
screenshots.forEach((asset, idx) => {
const ext = asset.mimeType === 'image/png' ? 'png' : 'jpg';
formData.append(`file_${idx}`, {
uri: asset.uri,
name: `screenshot_${idx}.${ext}`,
type: asset.mimeType,
} as any);
});
}
const res = await RestClient.post(`${RELAY}submitHelpIssue`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data as HelpIssueSubmitResponse;
} catch (err: any) {
console.log('🚀 ~ Relay ~ submitHelpIssue ~ err:', err);
Expand All @@ -764,6 +781,9 @@ export default class Relay {
if (err.code) {
throw new Error(err.code);
}
if (err.message) {
throw new Error(err.message);
}
throw new Error('An unexpected error occurred');
}
};
Expand Down
Loading