fix(v1.3): close 3 integration gaps from milestone audit
1. Push notifications: call sendPushToAll after streaming completes 2. Mobile offline: add useOfflineQueue + banners to MobileChatView 3. New conversation streaming: call startStream in Path 1 handleSend Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e7df7e5599
commit
d6f5e595d9
3 changed files with 34 additions and 2 deletions
|
|
@ -2,6 +2,7 @@ import { Router } from "express";
|
||||||
import type { Db } from "@paperclipai/db";
|
import type { Db } from "@paperclipai/db";
|
||||||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||||
import { chatService } from "../services/chat.js";
|
import { chatService } from "../services/chat.js";
|
||||||
|
import { sendPushToAll } from "../services/pushService.js";
|
||||||
import { issueService } from "../services/issues.js";
|
import { issueService } from "../services/issues.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
|
|
@ -117,6 +118,14 @@ export function chatRoutes(db: Db): Router {
|
||||||
agentId: agentId || undefined,
|
agentId: agentId || undefined,
|
||||||
});
|
});
|
||||||
res.write(`data: ${JSON.stringify({ done: true, messageId: message.id, content: fullContent.trim() })}\n\n`);
|
res.write(`data: ${JSON.stringify({ done: true, messageId: message.id, content: fullContent.trim() })}\n\n`);
|
||||||
|
|
||||||
|
// Fire push notification for offline subscribers (PWA-06)
|
||||||
|
const conversation = await svc.getConversation(req.params.id!);
|
||||||
|
sendPushToAll(db, conversation.companyId, {
|
||||||
|
title: "New agent response",
|
||||||
|
body: fullContent.trim().slice(0, 100),
|
||||||
|
data: { url: `/chat/${conversation.id}` },
|
||||||
|
}).catch(() => {}); // non-blocking
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (res.writable && !abort.signal.aborted) {
|
if (res.writable && !abort.signal.aborted) {
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,7 @@ export function ChatPanel() {
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConvo.id] });
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConvo.id] });
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat"] });
|
queryClient.invalidateQueries({ queryKey: ["chat"] });
|
||||||
// Note: streaming starts on next render when activeConversationId is set
|
startStream(content, resolvedAgentId ?? undefined);
|
||||||
// For now, the echo stream will be triggered by the new conversation
|
|
||||||
} else {
|
} else {
|
||||||
// Path 2: Active conversation -- post user message then stream
|
// Path 2: Active conversation -- post user message then stream
|
||||||
const message = await chatApi.postMessage(activeConversationId, { role: "user", content });
|
const message = await chatApi.postMessage(activeConversationId, { role: "user", content });
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { useStreamingChat } from "../hooks/useStreamingChat";
|
||||||
import { useBrainstormerDefault } from "../hooks/useBrainstormerDefault";
|
import { useBrainstormerDefault } from "../hooks/useBrainstormerDefault";
|
||||||
import { useChatBookmarks, useToggleBookmark } from "../hooks/useChatBookmarks";
|
import { useChatBookmarks, useToggleBookmark } from "../hooks/useChatBookmarks";
|
||||||
import { useChatFileUpload } from "../hooks/useChatFileUpload";
|
import { useChatFileUpload } from "../hooks/useChatFileUpload";
|
||||||
|
import { useOfflineQueue } from "../hooks/useOfflineQueue";
|
||||||
|
import { useOnlineStatus } from "../hooks/useOnlineStatus";
|
||||||
import { useChatConversations } from "../hooks/useChatConversations";
|
import { useChatConversations } from "../hooks/useChatConversations";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { chatApi } from "../api/chat";
|
import { chatApi } from "../api/chat";
|
||||||
|
|
@ -20,6 +22,9 @@ import { ChatAgentSelector } from "./ChatAgentSelector";
|
||||||
import { ChatConversationList } from "./ChatConversationList";
|
import { ChatConversationList } from "./ChatConversationList";
|
||||||
import { ChatStopButton } from "./ChatStopButton";
|
import { ChatStopButton } from "./ChatStopButton";
|
||||||
import { PullToRefresh } from "./PullToRefresh";
|
import { PullToRefresh } from "./PullToRefresh";
|
||||||
|
import { OfflineBanner } from "./OfflineBanner";
|
||||||
|
import { InstallPromptBanner } from "./InstallPromptBanner";
|
||||||
|
import { NotificationPermissionPrompt } from "./NotificationPermissionPrompt";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { AgentRole } from "@paperclipai/shared";
|
import type { AgentRole } from "@paperclipai/shared";
|
||||||
|
|
||||||
|
|
@ -41,6 +46,8 @@ export function MobileChatView() {
|
||||||
const { messages } = useChatMessages(activeConversationId);
|
const { messages } = useChatMessages(activeConversationId);
|
||||||
const { streamingContent, isStreaming, startStream, stop } = useStreamingChat(activeConversationId);
|
const { streamingContent, isStreaming, startStream, stop } = useStreamingChat(activeConversationId);
|
||||||
const { pendingFiles, addFile, removeFile, clearCompleted, completedFileIds } = useChatFileUpload(activeConversationId);
|
const { pendingFiles, addFile, removeFile, clearCompleted, completedFileIds } = useChatFileUpload(activeConversationId);
|
||||||
|
const { enqueue, queuedCount } = useOfflineQueue();
|
||||||
|
const isOnline = useOnlineStatus();
|
||||||
const brainstormerDefaultId = useBrainstormerDefault();
|
const brainstormerDefaultId = useBrainstormerDefault();
|
||||||
|
|
||||||
// useChatConversations for refetch (pull-to-refresh)
|
// useChatConversations for refetch (pull-to-refresh)
|
||||||
|
|
@ -103,8 +110,20 @@ export function MobileChatView() {
|
||||||
[activeConversationId, queryClient, pushToast],
|
[activeConversationId, queryClient, pushToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [agentResponseCount, setAgentResponseCount] = useState(0);
|
||||||
|
|
||||||
const handleSend = async (content: string) => {
|
const handleSend = async (content: string) => {
|
||||||
if (!selectedCompanyId) return;
|
if (!selectedCompanyId) return;
|
||||||
|
|
||||||
|
// If offline, enqueue the message and show a toast
|
||||||
|
if (!isOnline) {
|
||||||
|
if (activeConversationId) {
|
||||||
|
await enqueue(activeConversationId, content);
|
||||||
|
pushToast({ title: "Message queued — will send when you're back online", tone: "info" });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedAgentId = resolveAgentFromContent(content, agents, effectiveBrainstormerId);
|
const resolvedAgentId = resolveAgentFromContent(content, agents, effectiveBrainstormerId);
|
||||||
const fileIdsToAttach = [...completedFileIds];
|
const fileIdsToAttach = [...completedFileIds];
|
||||||
|
|
||||||
|
|
@ -122,6 +141,7 @@ export function MobileChatView() {
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConvo.id] });
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConvo.id] });
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat"] });
|
queryClient.invalidateQueries({ queryKey: ["chat"] });
|
||||||
|
startStream(content, resolvedAgentId ?? undefined);
|
||||||
} else {
|
} else {
|
||||||
const message = await chatApi.postMessage(activeConversationId, { role: "user", content });
|
const message = await chatApi.postMessage(activeConversationId, { role: "user", content });
|
||||||
if (fileIdsToAttach.length > 0) {
|
if (fileIdsToAttach.length > 0) {
|
||||||
|
|
@ -199,6 +219,8 @@ export function MobileChatView() {
|
||||||
if (!activeConversationId) {
|
if (!activeConversationId) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-40 flex h-[100dvh] flex-col bg-background">
|
<div className="fixed inset-0 z-40 flex h-[100dvh] flex-col bg-background">
|
||||||
|
<OfflineBanner queuedCount={queuedCount} />
|
||||||
|
<InstallPromptBanner />
|
||||||
{/* pb-16 accounts for the existing MobileBottomNav (h-16) at the bottom of Layout */}
|
{/* pb-16 accounts for the existing MobileBottomNav (h-16) at the bottom of Layout */}
|
||||||
<div className="flex-1 overflow-hidden pb-16">
|
<div className="flex-1 overflow-hidden pb-16">
|
||||||
{selectedCompanyId ? (
|
{selectedCompanyId ? (
|
||||||
|
|
@ -218,6 +240,8 @@ export function MobileChatView() {
|
||||||
// Active conversation view
|
// Active conversation view
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-40 flex h-[100dvh] flex-col bg-background">
|
<div className="fixed inset-0 z-40 flex h-[100dvh] flex-col bg-background">
|
||||||
|
<OfflineBanner queuedCount={queuedCount} />
|
||||||
|
<NotificationPermissionPrompt agentResponseCount={agentResponseCount} />
|
||||||
{/* Header: 48px tall */}
|
{/* Header: 48px tall */}
|
||||||
<div className="flex h-12 flex-shrink-0 items-center gap-2 border-b border-border px-3">
|
<div className="flex h-12 flex-shrink-0 items-center gap-2 border-b border-border px-3">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue