Mobile Solutions


Knowledge Check Answers

  1. useState returns an array of two values: the current state value and a function to update it. Example: const [messages, setMessages] = useState([]).

  2. useEffect with [] calls fetchMessages once when the component first loads. Calling it manually (e.g., after sending a message) is for refreshing the list immediately.

  3. FlatList is virtualized — it only renders the items visible on screen. ScrollView renders all items at once, which is slow for long chat histories.

  4. response.json() parses the HTTP response body from JSON format into a JavaScript object.

  5. It adapts keyboard behavior per platform: iOS uses "padding" (resize the view), Android uses "height" (resize with height adjustment).


Challenge 1 Solution

Add to the state:

const handleClearChat = async () => {
  try {
    await fetch(`${API_BASE_URL}/api/messages`, { method: "DELETE" });
    setMessages([]);
  } catch (err) {
    console.error("Failed to clear:", err);
  }
};

Add a button in the header:

<View style={styles.header}>
  <Text style={styles.headerTitle}>Agentic Chat</Text>
  <TouchableOpacity onPress={handleClearChat}>
    <Text style={{ color: "#FFF", fontSize: 14 }}>Clear</Text>
  </TouchableOpacity>
</View>

Update header style:

header: {
  backgroundColor: "#007AFF",
  paddingTop: 50,
  paddingBottom: 15,
  paddingHorizontal: 16,
  flexDirection: "row",
  justifyContent: "space-between",
  alignItems: "center",
},

Challenge 2 Solution

Add an avatar next to each bubble:

const renderMessage = ({ item }: { item: Message }) => {
  const isUser = item.sender === "user";
  return (
    <View style={[styles.messageRow, isUser && styles.messageRowUser]}>
      {!isUser && (
        <View style={styles.avatar}>
          <Text style={styles.avatarText}>A</Text>
        </View>
      )}
      <View style={[styles.bubble, isUser ? styles.userBubble : styles.aiBubble]}>
        <Text style={[styles.bubbleText, isUser ? styles.userText : styles.aiText]}>
          {item.content}
        </Text>
      </View>
      {isUser && (
        <View style={[styles.avatar, styles.userAvatar]}>
          <Text style={[styles.avatarText, styles.userAvatarText]}>U</Text>
        </View>
      )}
    </View>
  );
};

Styles:

messageRow: { flexDirection: "row", marginBottom: 8, alignItems: "flex-end" },
messageRowUser: { justifyContent: "flex-end" },
avatar: {
  width: 30,
  height: 30,
  borderRadius: 15,
  backgroundColor: "#DDD",
  justifyContent: "center",
  alignItems: "center",
  marginHorizontal: 6,
},
userAvatar: { backgroundColor: "#007AFF" },
avatarText: { color: "#666", fontWeight: "bold", fontSize: 14 },
userAvatarText: { color: "#FFF" },

Challenge 3 Solution

const [showScrollButton, setShowScrollButton] = useState(false);

const handleScroll = (event: any) => {
  const offsetY = event.nativeEvent.contentOffset.y;
  setShowScrollButton(offsetY > 200);
};

const scrollToBottom = () => {
  flatListRef.current?.scrollToEnd({ animated: true });
};

Add to FlatList:

<FlatList
  onScroll={handleScroll}
  scrollEventThrottle={100}
  ...
/>

Add floating button:

{showScrollButton && (
  <TouchableOpacity style={styles.scrollButton} onPress={scrollToBottom}>
    <Text style={styles.scrollButtonText}>↓</Text>
  </TouchableOpacity>
)}

Styles:

scrollButton: {
  position: "absolute",
  bottom: 80,
  right: 16,
  backgroundColor: "#007AFF",
  width: 40,
  height: 40,
  borderRadius: 20,
  justifyContent: "center",
  alignItems: "center",
  elevation: 4,
  shadowColor: "#000",
  shadowOpacity: 0.2,
  shadowRadius: 4,
},
scrollButtonText: { color: "#FFF", fontSize: 20 },

Open-Ended Project: Multi-Provider Chat

This is a larger project. Here's a starting approach:

1. Add provider state

const [provider, setProvider] = useState("openai");
const [customApiKey, setCustomApiKey] = useState("");
const [showSettings, setShowSettings] = useState(false);

2. Settings modal

{showSettings && (
  <View style={styles.settingsOverlay}>
    <View style={styles.settingsPanel}>
      <Text style={styles.settingsTitle}>Settings</Text>
      <Text>Provider:</Text>
      {["openai", "anthropic", "deepseek"].map((p) => (
        <TouchableOpacity key={p} onPress={() => setProvider(p)}>
          <Text style={{ fontWeight: provider === p ? "bold" : "normal" }}>{p}</Text>
        </TouchableOpacity>
      ))}
      <TextInput
        placeholder="API Key"
        value={customApiKey}
        onChangeText={setCustomApiKey}
        secureTextEntry
      />
      <TouchableOpacity onPress={() => setShowSettings(false)}>
        <Text>Done</Text>
      </TouchableOpacity>
    </View>
  </View>
)}

3. Send provider info in request headers

const response = await fetch(`${API_BASE_URL}/api/chat`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Provider": provider,
    "X-Api-Key": customApiKey,
  },
  body: JSON.stringify({ message: text }),
});

4. Backend reads headers (in routes.py)

provider = request.headers.get("X-Provider", "openai")
api_key = request.headers.get("X-Api-Key") or os.getenv("OPENAI_API_KEY")

if provider == "anthropic":
    # Call Anthropic API
elif provider == "deepseek":
    # Call DeepSeek API (OpenAI-compatible)
else:
    # Default OpenAI call