حلول الجوال


إجابات اختبار المعرفة

  1. تُرجع useState مصفوفة من قيمتين: قيمة الحالة الحالية ودالة لتحديثها. مثال: const [messages, setMessages] = useState([]).

  2. useEffect مع [] يستدعي fetchMessages مرة واحدة عند أول تحميل للمكوّن. أما استدعاؤه يدويًا (مثلًا بعد إرسال رسالة) فيهدف إلى تحديث القائمة فورًا.

  3. تعرض FlatList العناصر افتراضيًا (virtualization) — فلا تعرض إلا العناصر الظاهرة على الشاشة. أما ScrollView فتعرض كل العناصر دفعة واحدة، وهو أمر بطيء مع سجلات المحادثات الطويلة.

  4. response.json() تحوّل جسم استجابة HTTP من صيغة JSON إلى كائن JavaScript.

  5. يتكيّف سلوك لوحة المفاتيح حسب المنصة: يستخدم iOS القيمة "padding" (تغيير حجم العرض)، ويستخدم Android القيمة "height" (تغيير الحجم مع تعديل الارتفاع).


حل التحدي 1

أضف إلى منطق الحالة:

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

أضف زرًا في الترويسة:

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

حدّث نمط الترويسة:

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

حل التحدي 2

أضف صورة رمزية بجانب كل فقاعة:

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>
  );
};

الأنماط:

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" },

حل التحدي 3

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 });
};

أضف إلى FlatList:

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

أضف زرًا عائمًا:

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

الأنماط:

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 },

مشروع مفتوح: محادثة متعددة المزوّدين

هذا مشروع أكبر. إليك مقاربة أولية للبدء:

1. إضافة حالة المزوّد

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

2. نافذة الإعدادات المنبثقة

{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. إرسال معلومات المزوّد في ترويسات الطلب

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. قراءة الترويسات في الواجهة الخلفية (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":
    # استدعاء Anthropic API
elif provider == "deepseek":
    # استدعاء DeepSeek API (متوافق مع OpenAI)
else:
    # الاستدعاء الافتراضي لـ OpenAI