Skip to content
Merged
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
68 changes: 65 additions & 3 deletions src/api/projectHandling.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,22 +165,84 @@ export const fetchDecryptedSecret = async (configId) => {
}
};

export const saveChatMessage = async (projectId, message) => {
export const saveChatMessage = async (
projectId,
message,
{ isServerConnected, isAuthenticated, userId } = {},
) => {
try {
await axios.post(`${API_URL}/api/projects/${projectId}/chat/save`, {
message,
});

// Sync full chat history to cloud after local save
if (isServerConnected && isAuthenticated && userId) {
try {
const history = await loadChatHistory(projectId);
await axios.put(
`${import.meta.env.VITE_admin_server}/api/ai-chat/${userId}/${projectId}`,
{ messages: history },
{ withCredentials: true },
);
} catch (syncError) {
console.log("Failed to sync chat to cloud:", syncError.message);
}
}
} catch (error) {
console.error("Failed to save chat message:", error);
}
};

export const loadChatHistory = async (projectId) => {
export const loadChatHistory = async (
projectId,
{ isServerConnected, isAuthenticated, userId } = {},
) => {
try {
const response = await axios.get(
`${API_URL}/api/projects/${projectId}/chat`,
);
return response.data.history || [];
const localHistory = response.data.history || [];

if (isServerConnected && isAuthenticated && userId) {
if (localHistory.length === 0) {
// Pull from cloud if local is empty (e.g. new device)
try {
const cloudRes = await axios.get(
`${import.meta.env.VITE_admin_server}/api/ai-chat/${userId}/${projectId}`,
{ withCredentials: true },
);
const cloudMessages = cloudRes.data.messages || [];
if (cloudMessages.length > 0) {
for (const msg of cloudMessages) {
await axios.post(
`${API_URL}/api/projects/${projectId}/chat/save`,
{ message: msg },
);
}
return cloudMessages;
}
} catch (cloudError) {
console.log("Failed to fetch chat from cloud:", cloudError.message);
}
} else {
// Push existing local history to cloud (initial sync for pre-existing chat.json)
try {
await axios.put(
`${import.meta.env.VITE_admin_server}/api/ai-chat/${userId}/${projectId}`,
{ messages: localHistory },
{ withCredentials: true },
);
console.log("✅ Synced existing chat history to cloud");
} catch (syncError) {
console.log(
"Failed to sync existing chat to cloud:",
syncError.message,
);
}
}
}

return localHistory;
} catch (error) {
console.error("Failed to load chat history:", error);
return [];
Expand Down
21 changes: 16 additions & 5 deletions src/components/aiChatPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ConfirmModal from "./confirmModal";

const AIChatPanel = ({ projectDetails, sections, onApplyChanges, onClose }) => {
const { updateProjectDetails } = useContext(projectContext);
const { isAuthenticated } = useAuth();
const { isAuthenticated, user, isServerConnected } = useAuth();
const { settings } = useSettings();
const navigate = useNavigate();
const [input, setInput] = useState("");
Expand All @@ -28,6 +28,13 @@ const AIChatPanel = ({ projectDetails, sections, onApplyChanges, onClose }) => {
// Derived: input should be disabled when loading or streaming
const isBusy = isLoading || streamingMsgId !== null;

// Auth context for cloud sync
const syncOptions = {
isServerConnected,
isAuthenticated,
userId: user?.userId,
};

// ---- Typewriter effect component ----
const TypewriterText = ({ text, onComplete }) => {
const [displayedText, setDisplayedText] = useState("");
Expand Down Expand Up @@ -110,7 +117,7 @@ const AIChatPanel = ({ projectDetails, sections, onApplyChanges, onClose }) => {
// 1. Load History on Mount
useEffect(() => {
if (projectId) {
loadChatHistory(projectId).then((history) => {
loadChatHistory(projectId, syncOptions).then((history) => {
if (history.length > 0) {
setMessages(history);
} else {
Expand All @@ -124,7 +131,7 @@ const AIChatPanel = ({ projectDetails, sections, onApplyChanges, onClose }) => {
}),
};
setMessages([welcomeMsg]);
saveChatMessage(projectId, welcomeMsg);
saveChatMessage(projectId, welcomeMsg, syncOptions);
}
});
}
Expand All @@ -139,7 +146,7 @@ const AIChatPanel = ({ projectDetails, sections, onApplyChanges, onClose }) => {
// Helper to add message to state AND save to backend
const addMessage = (msg) => {
setMessages((prev) => [...prev, msg]);
saveChatMessage(projectId, msg);
saveChatMessage(projectId, msg, syncOptions);
};

const handleSend = async (e) => {
Expand Down Expand Up @@ -237,7 +244,11 @@ const AIChatPanel = ({ projectDetails, sections, onApplyChanges, onClose }) => {
};
setMessages((prev) => [...prev, aiMsg]);
setStreamingMsgId(aiMsgId); // Start typewriter
saveChatMessage(projectId, { ...aiMsg, isStreaming: false });
saveChatMessage(
projectId,
{ ...aiMsg, isStreaming: false },
syncOptions,
);
}
} catch (error) {
if (error.name === "AbortError") {
Expand Down