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
25 changes: 25 additions & 0 deletions src/app/api/chat/index+api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,34 @@ export async function POST(request: Request) {
...(previousResponseId && { previous_response_id: previousResponseId }),
});

let relatedQuestions: string[] | undefined = undefined;
try {
const questionsCompletion = await openai.chat.completions.create({
model: 'gpt-4.1',
messages: [
{
role: 'user',
content:
`Provide 3 advanced follow-up questions related to: ${message}. ` +
'Respond in JSON format as { "questions": ["q1","q2","q3"] }.',
},
],
response_format: { type: 'json_object' },
});
const parsed = JSON.parse(
questionsCompletion.choices[0].message.content || '{}'
);
if (Array.isArray(parsed.questions)) {
relatedQuestions = parsed.questions;
}
} catch (err) {
console.error('Failed to generate related questions:', err);
}

return Response.json({
responseMessage: response.output_text,
responseId: response.id,
...(relatedQuestions && { relatedQuestions }),
});
} catch (error) {
console.error('OpenAI error:', error);
Expand Down
25 changes: 25 additions & 0 deletions src/app/api/chat/speech+api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,35 @@ export async function POST(request: Request) {
...(previousResponseId && { previous_response_id: previousResponseId }),
});

let relatedQuestions: string[] | undefined = undefined;
try {
const questionsCompletion = await openai.chat.completions.create({
model: 'gpt-4.1',
messages: [
{
role: 'user',
content:
`Provide 3 advanced follow-up questions related to: ${transcription.text}. ` +
'Respond in JSON format as { "questions": ["q1","q2","q3"] }.',
},
],
response_format: { type: 'json_object' },
});
const parsed = JSON.parse(
questionsCompletion.choices[0].message.content || '{}'
);
if (Array.isArray(parsed.questions)) {
relatedQuestions = parsed.questions;
}
} catch (err) {
console.error('Failed to generate related questions:', err);
}

return Response.json({
responseId: response.id,
responseMessage: response.output_text,
transcribedMessage: transcription.text,
...(relatedQuestions && { relatedQuestions }),
});
} catch (error) {
console.log(error);
Expand Down
30 changes: 22 additions & 8 deletions src/app/chat/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
getTextResponse,
createAIImage,
getSpeechResponse,
type ChatResponse,
} from '@/services/chatService';
import type { Message } from '@/types/types';

export default function ChatScreen() {
const { id } = useLocalSearchParams();
Expand All @@ -29,6 +31,10 @@ export default function ChatScreen() {

const addNewMessage = useChatStore((state) => state.addNewMessage);

const handleQuestionPress = async (question: string) => {
await handleSend(question, null, false, null);
};

useEffect(() => {
const timeout = setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
Expand Down Expand Up @@ -60,7 +66,7 @@ export default function ChatScreen() {
)?.responseId;

try {
let data;
let data: ChatResponse | { image: string };
if (audioBase64) {
data = await getSpeechResponse(audioBase64, previousResponseId);
const myMessage = {
Expand All @@ -75,13 +81,16 @@ export default function ChatScreen() {
data = await getTextResponse(message, imageBase64, previousResponseId);
}

const aiResponseMessage = {
const aiResponseMessage: Message = {
id: Date.now().toString(),
message: data.responseMessage,
responseId: data.responseId,
image: data.image,
role: 'assistant' as const,
};
role: 'assistant',
...("responseMessage" in data && {
message: data.responseMessage,
responseId: data.responseId,
relatedQuestions: data.relatedQuestions,
}),
...("image" in data && { image: data.image }),
} as Message;

addNewMessage(chat.id, aiResponseMessage);
} catch (error) {
Expand All @@ -104,7 +113,12 @@ export default function ChatScreen() {
<FlatList
ref={flatListRef}
data={chat.messages}
renderItem={({ item }) => <MessageListItem messageItem={item} />}
renderItem={({ item }) => (
<MessageListItem
messageItem={item}
onQuestionPress={handleQuestionPress}
/>
)}
ListFooterComponent={() =>
isWaitingForResponse && (
<Text className='text-gray-400 px-6 mb-4 animate-pulse'>
Expand Down
19 changes: 12 additions & 7 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
createAIImage,
getSpeechResponse,
getTextResponse,
type ChatResponse,
} from '@/services/chatService';
import type { Message } from '@/types/types';

export default function HomeScreen() {
const createNewChat = useChatStore((state) => state.createNewChat);
Expand Down Expand Up @@ -36,7 +38,7 @@ export default function HomeScreen() {
router.push(`/chat/${newChatId}`);

try {
let data;
let data: ChatResponse | { image: string };
if (audioBase64) {
data = await getSpeechResponse(audioBase64);
const myMessage = {
Expand All @@ -51,13 +53,16 @@ export default function HomeScreen() {
data = await getTextResponse(message, imageBase64);
}

const aiResponseMessage = {
const aiResponseMessage: Message = {
id: Date.now().toString(),
message: data.responseMessage,
responseId: data.responseId,
image: data.image,
role: 'assistant' as const,
};
role: 'assistant',
...("responseMessage" in data && {
message: data.responseMessage,
responseId: data.responseId,
relatedQuestions: data.relatedQuestions,
}),
...("image" in data && { image: data.image }),
} as Message;

addNewMessage(newChatId, aiResponseMessage);

Expand Down
31 changes: 28 additions & 3 deletions src/components/MessageListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Text, View, Image } from 'react-native';
import { Text, View, Image, Pressable } from 'react-native';
import Markdown from 'react-native-markdown-display';
import { Message } from '@/types/types';
import { markdownStyles } from '@/utils/markdown';

interface MessageListItemProps {
messageItem: Message;
onQuestionPress?: (question: string) => void;
}

export default function MessageListItem({ messageItem }: MessageListItemProps) {
const { message, role, image } = messageItem;
export default function MessageListItem({
messageItem,
onQuestionPress,
}: MessageListItemProps) {
const { message, role, image, relatedQuestions } = messageItem;
const isUser = role === 'user';

return (
Expand All @@ -32,6 +36,27 @@ export default function MessageListItem({ messageItem }: MessageListItemProps) {
<Markdown style={markdownStyles}>{message}</Markdown>
</View>
)}
{!isUser && relatedQuestions?.length ? (
<View className='mt-2 w-full max-w-[80%]'>
<Text className='text-gray-400 text-xs font-semibold mb-1'>
Related questions
</Text>
<View className='space-y-1'>
{relatedQuestions.map((q, idx) => {
const display = q.length > 80 ? `${q.slice(0, 77)}...` : q;
return (
<Pressable
key={idx}
onPress={() => onQuestionPress?.(q)}
className='bg-[#262626] rounded-lg p-2'
>
<Text className='text-gray-300 text-sm'>{display}</Text>
</Pressable>
);
})}
</View>
</View>
) : null}
</View>
);
}
18 changes: 13 additions & 5 deletions src/services/chatService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export async function createAIImage(prompt: string) {
export interface ChatResponse {
responseMessage: string;
responseId: string;
image?: string;
transcribedMessage?: string;
relatedQuestions?: string[];
}

export async function createAIImage(prompt: string): Promise<{ image: string }> {
const res = await fetch('/api/chat/createImage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -14,7 +22,7 @@ export const getTextResponse = async (
message: string,
imageBase64: string | null,
previousResponseId?: string
) => {
): Promise<ChatResponse> => {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -23,13 +31,13 @@ export const getTextResponse = async (

const data = await res.json();
if (!res.ok) throw new Error(data.error);
return data;
return data as ChatResponse;
};

export const getSpeechResponse = async (
audioBase64: string,
previousResponseId?: string
) => {
): Promise<ChatResponse> => {
const res = await fetch('/api/chat/speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -38,5 +46,5 @@ export const getSpeechResponse = async (

const data = await res.json();
if (!res.ok) throw new Error(data.error);
return data;
return data as ChatResponse;
};
1 change: 1 addition & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Message {
message?: string;
responseId?: string;
image?: string;
relatedQuestions?: string[];
}

export interface Chat {
Expand Down