Merge pull request #9 from squaalldev/codex/analyze-.py-files-for-react-migration-vk9f81
Browse filesFastAPI backend + React frontend integration, google-genai, and multi-stage Docker for HF Spaces
frontend/src/components/ChatWindow.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React, { useEffect, useState } from 'react';
|
| 2 |
|
| 3 |
interface Message {
|
| 4 |
role: 'user' | 'assistant' | 'error';
|
|
@@ -36,10 +36,47 @@ function toUiMessage(msg: ApiMessage, idx: number): Message {
|
|
| 36 |
};
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
| 40 |
const [messages, setMessages] = useState<Message[]>([]);
|
| 41 |
const [input, setInput] = useState('');
|
| 42 |
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
const withUid = (path: string) => `${path}${path.includes('?') ? '&' : '?'}uid=${encodeURIComponent(uid)}`;
|
| 45 |
|
|
@@ -55,15 +92,21 @@ function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
|
| 55 |
|
| 56 |
useEffect(() => {
|
| 57 |
if (!chatId) {
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
return;
|
| 60 |
}
|
| 61 |
|
|
|
|
|
|
|
| 62 |
loadMessages(chatId).catch((error) => {
|
| 63 |
console.error(error);
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
});
|
| 66 |
-
}, [chatId]);
|
| 67 |
|
| 68 |
const animateAssistantText = async (messageId: string, fullText: string) => {
|
| 69 |
let partial = '';
|
|
@@ -84,6 +127,8 @@ function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
|
| 84 |
};
|
| 85 |
|
| 86 |
const sendToCurrentChat = async (content: string, isExample = false) => {
|
|
|
|
|
|
|
| 87 |
const activeChatId = await onEnsureChat();
|
| 88 |
|
| 89 |
const userMessage: Message = {
|
|
@@ -94,7 +139,6 @@ function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
|
| 94 |
};
|
| 95 |
|
| 96 |
setMessages((prev) => [...prev, userMessage]);
|
| 97 |
-
setLoading(true);
|
| 98 |
|
| 99 |
try {
|
| 100 |
const response = await fetch(withUid(`/api/chats/${activeChatId}/messages`), {
|
|
@@ -133,6 +177,7 @@ function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
|
| 133 |
]);
|
| 134 |
} finally {
|
| 135 |
setLoading(false);
|
|
|
|
| 136 |
}
|
| 137 |
};
|
| 138 |
|
|
@@ -172,9 +217,7 @@ function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
|
| 172 |
<span className="avatar">{msg.avatar}</span>
|
| 173 |
<div className="content">
|
| 174 |
{msg.role === 'assistant' ? (
|
| 175 |
-
<div className="assistant-markdown">
|
| 176 |
-
{msg.content}
|
| 177 |
-
</div>
|
| 178 |
) : (
|
| 179 |
<div className="user-plain">{msg.content}</div>
|
| 180 |
)}
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
| 2 |
|
| 3 |
interface Message {
|
| 4 |
role: 'user' | 'assistant' | 'error';
|
|
|
|
| 36 |
};
|
| 37 |
}
|
| 38 |
|
| 39 |
+
function formatInline(text: string) {
|
| 40 |
+
const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*)/g).filter(Boolean);
|
| 41 |
+
return parts.map((part, idx) => {
|
| 42 |
+
if (part.startsWith('**') && part.endsWith('**')) {
|
| 43 |
+
return <strong key={`b-${idx}`}>{part.slice(2, -2)}</strong>;
|
| 44 |
+
}
|
| 45 |
+
if (part.startsWith('*') && part.endsWith('*')) {
|
| 46 |
+
return <em key={`i-${idx}`}>{part.slice(1, -1)}</em>;
|
| 47 |
+
}
|
| 48 |
+
return <React.Fragment key={`t-${idx}`}>{part}</React.Fragment>;
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function renderFormattedText(content: string) {
|
| 53 |
+
return content.split('\n').map((line, idx) => (
|
| 54 |
+
<p key={`line-${idx}`}>{formatInline(line)}</p>
|
| 55 |
+
));
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
|
| 59 |
const [messages, setMessages] = useState<Message[]>([]);
|
| 60 |
const [input, setInput] = useState('');
|
| 61 |
const [loading, setLoading] = useState(false);
|
| 62 |
+
const sendingRef = useRef(false);
|
| 63 |
+
|
| 64 |
+
const withUid = useMemo(
|
| 65 |
+
() => (path: string) => `${path}${path.includes('?') ? '&' : '?'}uid=${encodeURIComponent(uid)}`,
|
| 66 |
+
[uid],
|
| 67 |
+
);
|
| 68 |
+
|
| 69 |
+
const loadMessages = async (targetChatId: string) => {
|
| 70 |
+
const response = await fetch(withUid(`/api/chats/${targetChatId}/messages`));
|
| 71 |
+
if (!response.ok) {
|
| 72 |
+
throw new Error('No se pudo cargar el historial');
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const data: ApiMessage[] = await response.json();
|
| 76 |
+
if (!sendingRef.current) {
|
| 77 |
+
setMessages(data.map((msg, idx) => toUiMessage(msg, idx)));
|
| 78 |
+
}
|
| 79 |
+
};
|
| 80 |
|
| 81 |
const withUid = (path: string) => `${path}${path.includes('?') ? '&' : '?'}uid=${encodeURIComponent(uid)}`;
|
| 82 |
|
|
|
|
| 92 |
|
| 93 |
useEffect(() => {
|
| 94 |
if (!chatId) {
|
| 95 |
+
if (!sendingRef.current) {
|
| 96 |
+
setMessages([]);
|
| 97 |
+
}
|
| 98 |
return;
|
| 99 |
}
|
| 100 |
|
| 101 |
+
if (sendingRef.current) return;
|
| 102 |
+
|
| 103 |
loadMessages(chatId).catch((error) => {
|
| 104 |
console.error(error);
|
| 105 |
+
if (!sendingRef.current) {
|
| 106 |
+
setMessages([]);
|
| 107 |
+
}
|
| 108 |
});
|
| 109 |
+
}, [chatId, withUid]);
|
| 110 |
|
| 111 |
const animateAssistantText = async (messageId: string, fullText: string) => {
|
| 112 |
let partial = '';
|
|
|
|
| 127 |
};
|
| 128 |
|
| 129 |
const sendToCurrentChat = async (content: string, isExample = false) => {
|
| 130 |
+
sendingRef.current = true;
|
| 131 |
+
setLoading(true);
|
| 132 |
const activeChatId = await onEnsureChat();
|
| 133 |
|
| 134 |
const userMessage: Message = {
|
|
|
|
| 139 |
};
|
| 140 |
|
| 141 |
setMessages((prev) => [...prev, userMessage]);
|
|
|
|
| 142 |
|
| 143 |
try {
|
| 144 |
const response = await fetch(withUid(`/api/chats/${activeChatId}/messages`), {
|
|
|
|
| 177 |
]);
|
| 178 |
} finally {
|
| 179 |
setLoading(false);
|
| 180 |
+
sendingRef.current = false;
|
| 181 |
}
|
| 182 |
};
|
| 183 |
|
|
|
|
| 217 |
<span className="avatar">{msg.avatar}</span>
|
| 218 |
<div className="content">
|
| 219 |
{msg.role === 'assistant' ? (
|
| 220 |
+
<div className="assistant-markdown">{renderFormattedText(msg.content)}</div>
|
|
|
|
|
|
|
| 221 |
) : (
|
| 222 |
<div className="user-plain">{msg.content}</div>
|
| 223 |
)}
|