File size: 3,848 Bytes
0e11366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { useCallback, useRef, useState } from "react";
import { CHAT_URL, UPLOAD_URL } from "../lib/constants";
import type { Message } from "../lib/types";

export function useChat(
  sessionId: string,
  selectedLL: [number, number] | null
) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [draft, setDraft] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const [hasFirstToken, setHasFirstToken] = useState(false);
  const [pendingPhotoUrl, setPendingPhotoUrl] = useState<string | null>(null);
  const [isUploading, setIsUploading] = useState(false);
  const chatBodyRef = useRef<HTMLDivElement | null>(null);

  const scrollToBottom = useCallback(() => {
    const el = chatBodyRef.current;
    if (!el) return;
    el.scrollTop = el.scrollHeight;
  }, []);

  const typeOut = useCallback(
    async (fullText: string) => {
      const step = fullText.length > 1200 ? 6 : fullText.length > 400 ? 3 : 1;
      const delayMs =
        fullText.length > 1200 ? 4 : fullText.length > 400 ? 8 : 15;
      let firstTokenSet = false;
      for (let i = 0; i < fullText.length; i += step) {
        const acc = fullText.slice(0, i + step);
        setMessages((m) => {
          const out = [...m];
          for (let j = out.length - 1; j >= 0; j--) {
            if (out[j].role === "assistant") {
              out[j] = { ...out[j], text: acc };
              break;
            }
          }
          return out;
        });
        if (!firstTokenSet && acc.length > 0) {
          setHasFirstToken(true);
          firstTokenSet = true;
        }
        scrollToBottom();
        await new Promise((r) => setTimeout(r, delayMs));
      }
      setIsStreaming(false);
      setHasFirstToken(true);
      scrollToBottom();
    },
    [scrollToBottom]
  );

  const onFileChosen = useCallback(async (file: File) => {
    setIsUploading(true);
    try {
      const fd = new FormData();
      fd.append("file", file);
      const res = await fetch(UPLOAD_URL, { method: "POST", body: fd }).then(
        (r) => r.json()
      );
      const url =
        res?.url ||
        (res?.path
          ? (import.meta.env.VITE_API_BASE || "http://localhost:8000") +
            res.path
          : "");
      if (url) setPendingPhotoUrl(url);
    } finally {
      setIsUploading(false);
    }
  }, []);

  const send = useCallback(async () => {
    const text = draft.trim();
    if (!text) return;

    const attached = pendingPhotoUrl; // capture now
    setPendingPhotoUrl(null); // clear immediately
    setMessages((m) => [
      ...m,
      { role: "user", text, image: attached || undefined },
    ]);
    setDraft("");
    setTimeout(scrollToBottom, 0);

    setIsStreaming(true);
    setHasFirstToken(false);
    setMessages((m) => [...m, { role: "assistant", text: "" }]);
    setTimeout(scrollToBottom, 0);

    let finalText = text;
    if (selectedLL)
      finalText += `\n\n[COORDS lat=${selectedLL[0]} lon=${selectedLL[1]}]`;

    const payload: any = { message: finalText, session_id: sessionId };
    if (selectedLL)
      payload.user_location = { lat: selectedLL[0], lon: selectedLL[1] };
    if (attached) payload.photo_url = attached;

    const res = await fetch(CHAT_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    })
      .then((r) => r.json())
      .catch(() => ({ reply: "Something went wrong." }));

    await typeOut(res.reply || "(no reply)");
    return res; // caller can react to tool_used (e.g., reload reports)
  }, [draft, pendingPhotoUrl, selectedLL, sessionId, scrollToBottom, typeOut]);

  return {
    messages,
    draft,
    setDraft,
    isStreaming,
    hasFirstToken,
    chatBodyRef,
    send,
    pendingPhotoUrl,
    setPendingPhotoUrl,
    isUploading,
    onFileChosen,
  };
}