feat(chat): migrate chat to backend API
This commit is contained in:
@@ -6,32 +6,57 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { getChatSessionById, listChatMessages } from "@/lib/api"
|
||||||
import { sendImageMessage, sendTextMessage } from "@/lib/api/chat"
|
import { sendImageMessage, sendTextMessage } from "@/lib/api/chat"
|
||||||
import { notifyInfo } from "@/lib/toast"
|
import { notifyInfo } from "@/lib/toast"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
|
||||||
import { ArrowLeft, ImagePlus, Lock, Send } from "lucide-react"
|
import { ArrowLeft, ImagePlus, Lock, Send } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { use, useMemo, useRef, useState } from "react"
|
import { use, useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const session = useChatStore((state) => state.sessions.find((item) => item.id === id))
|
const [loading, setLoading] = useState(true)
|
||||||
const allMessages = useChatStore((state) => state.messages)
|
const [session, setSession] = useState<
|
||||||
// Filter logic runs here via useMemo rather than inside the Zustand selector.
|
Awaited<ReturnType<typeof getChatSessionById>> | undefined
|
||||||
// useSyncExternalStore requires a stable snapshot reference on each render.
|
>(undefined)
|
||||||
// Inline filter in a selector creates a new array per call and can trigger
|
const [messages, setMessages] = useState<Awaited<ReturnType<typeof listChatMessages>>>([])
|
||||||
// infinite re-render loops in Zustand v5 (pmndrs/zustand#1936).
|
|
||||||
const messages = useMemo(
|
|
||||||
() => allMessages.filter((item) => item.sessionId === id),
|
|
||||||
[allMessages, id],
|
|
||||||
)
|
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
const imageInputRef = useRef<HTMLInputElement>(null)
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setLoading(true)
|
||||||
|
Promise.all([Promise.resolve(getChatSessionById(id)), Promise.resolve(listChatMessages(id))])
|
||||||
|
.then(([nextSession, nextMessages]) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setSession(nextSession)
|
||||||
|
setMessages(nextMessages)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) return
|
||||||
|
setSession(undefined)
|
||||||
|
setMessages([])
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (cancelled) return
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">加载中...</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||||
@@ -147,11 +172,22 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const file = event.target.files?.[0]
|
const target = event.currentTarget
|
||||||
|
const file = target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const result = sendImageMessage(session.id, URL.createObjectURL(file))
|
const imageUrl = URL.createObjectURL(file)
|
||||||
if (!result.ok) notifyInfo(result.error.msg)
|
|
||||||
event.target.value = ""
|
Promise.resolve(sendImageMessage(session.id, imageUrl))
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.ok) {
|
||||||
|
notifyInfo(result.error.msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return Promise.resolve(listChatMessages(session.id)).then(setMessages)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
target.value = ""
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
@@ -161,12 +197,14 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
|
|||||||
const text = input.trim()
|
const text = input.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
||||||
const result = sendTextMessage(session.id, text)
|
Promise.resolve(sendTextMessage(session.id, text)).then((result) => {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
notifyInfo(result.error.msg)
|
notifyInfo(result.error.msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setInput("")
|
setInput("")
|
||||||
|
return Promise.resolve(listChatMessages(session.id)).then(setMessages)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -3,15 +3,33 @@
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { listChatSessions } from "@/lib/api"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
|
||||||
import { Lock, MessageSquare } from "lucide-react"
|
import { Lock, MessageSquare } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
export default function ChatListPage() {
|
export default function ChatListPage() {
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const [sessions, setSessions] = useState<Awaited<ReturnType<typeof listChatSessions>>>([])
|
||||||
const userId = useAuthStore((state) => state.user?.id)
|
const userId = useAuthStore((state) => state.user?.id)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const result = await Promise.resolve(listChatSessions())
|
||||||
|
if (!cancelled) setSessions(result)
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setSessions([])
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
|
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
|
||||||
<h1 className="text-2xl font-bold">消息</h1>
|
<h1 className="text-2xl font-bold">消息</h1>
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import OrderActions from "@/components/order-actions"
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { getOrderById, listReviewsByOrder } from "@/lib/api"
|
import { getOrderById, listChatSessions, listReviewsByOrder } from "@/lib/api"
|
||||||
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
|
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
import { useChatStore } from "@/store/chat"
|
|
||||||
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
|
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { use, useEffect, useState } from "react"
|
import { use, useEffect, useState } from "react"
|
||||||
@@ -34,7 +33,7 @@ const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept"
|
|||||||
|
|
||||||
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const [sessions, setSessions] = useState<Awaited<ReturnType<typeof listChatSessions>>>([])
|
||||||
const [reviews, setReviews] = useState<Awaited<ReturnType<typeof listReviewsByOrder>>>([])
|
const [reviews, setReviews] = useState<Awaited<ReturnType<typeof listReviewsByOrder>>>([])
|
||||||
const [order, setOrder] = useState<Awaited<ReturnType<typeof getOrderById>> | undefined>(
|
const [order, setOrder] = useState<Awaited<ReturnType<typeof getOrderById>> | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -42,6 +41,25 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [nowTs, setNowTs] = useState(0)
|
const [nowTs, setNowTs] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const sessions = await Promise.resolve(listChatSessions())
|
||||||
|
if (cancelled) return
|
||||||
|
setSessions(sessions)
|
||||||
|
} catch {
|
||||||
|
if (cancelled) return
|
||||||
|
setSessions([])
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { listOrders } from "@/lib/api"
|
import { listChatSessions, listOrders } from "@/lib/api"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import {
|
import {
|
||||||
isActiveOrder,
|
isActiveOrder,
|
||||||
@@ -16,7 +16,6 @@ import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
|
|||||||
import type { OrderStatus, UserRole } from "@/lib/types"
|
import type { OrderStatus, UserRole } from "@/lib/types"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
|
||||||
import { useShopStore } from "@/store/shops"
|
import { useShopStore } from "@/store/shops"
|
||||||
import { Clock, MessageSquare, RefreshCw } from "lucide-react"
|
import { Clock, MessageSquare, RefreshCw } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
@@ -83,7 +82,7 @@ function OrderListContent({
|
|||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
||||||
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
|
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const [sessions, setSessions] = useState<Awaited<ReturnType<typeof listChatSessions>>>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -104,6 +103,25 @@ function OrderListContent({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const items = await Promise.resolve(listChatSessions())
|
||||||
|
if (cancelled) return
|
||||||
|
setSessions(items)
|
||||||
|
} catch {
|
||||||
|
if (cancelled) return
|
||||||
|
setSessions([])
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const tabs =
|
const tabs =
|
||||||
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import type { ApiDecision } from "@/lib/errors"
|
|||||||
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
||||||
import type { Order, OrderStatus } from "@/lib/types"
|
import type { Order, OrderStatus } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
import { useShopStore } from "@/store/shops"
|
import { useShopStore } from "@/store/shops"
|
||||||
import {
|
import {
|
||||||
@@ -51,15 +50,12 @@ export default function OrderActions({
|
|||||||
const currentUserId = useAuthStore((state) => state.user?.id)
|
const currentUserId = useAuthStore((state) => state.user?.id)
|
||||||
const storeOrder = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
const storeOrder = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
||||||
const order = orderProp ?? storeOrder
|
const order = orderProp ?? storeOrder
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
|
||||||
const dispatchMode = useShopStore((state) => {
|
const dispatchMode = useShopStore((state) => {
|
||||||
if (!order?.shopId) return "manual"
|
if (!order?.shopId) return "manual"
|
||||||
const shop = state.shops.find((item) => item.id === order.shopId)
|
const shop = state.shops.find((item) => item.id === order.shopId)
|
||||||
return shop?.dispatchMode ?? "manual"
|
return shop?.dispatchMode ?? "manual"
|
||||||
})
|
})
|
||||||
const resolvedChatSessionId =
|
const resolvedChatSessionId = chatSessionId
|
||||||
chatSessionId ??
|
|
||||||
sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id
|
|
||||||
|
|
||||||
const status = order?.status ?? initialStatus
|
const status = order?.status ?? initialStatus
|
||||||
const isConsumer = order?.consumerId === currentUserId
|
const isConsumer = order?.consumerId === currentUserId
|
||||||
|
|||||||
+100
-41
@@ -1,55 +1,114 @@
|
|||||||
import { allow, deny } from "@/lib/decision"
|
import { allow, deny } from "@/lib/decision"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { isApiError, toApiError, type ApiDecision } from "@/lib/errors"
|
||||||
import { useChatStore } from "@/store/chat"
|
import type { ChatMessage, ChatSession } from "@/lib/types"
|
||||||
|
|
||||||
export function listChatSessions() {
|
import { httpJson } from "./http"
|
||||||
return useChatStore.getState().sessions
|
|
||||||
|
type Paginated<T> = {
|
||||||
|
items: T[]
|
||||||
|
meta: {
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChatSessionById(sessionId: string) {
|
export type ListChatSessionsOptions = {
|
||||||
return useChatStore.getState().sessions.find((session) => session.id === sessionId)
|
offset?: number
|
||||||
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listChatMessages(sessionId: string) {
|
export type ListChatMessagesOptions = {
|
||||||
return useChatStore.getState().messages.filter((message) => message.sessionId === sessionId)
|
offset?: number
|
||||||
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendTextMessage(sessionId: string, content: string) {
|
function withOffsetLimit(path: string, options?: { offset?: number; limit?: number }): string {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const offset = options?.offset ?? 0
|
||||||
if (!userId) return deny(401, "请先登录")
|
const limit = options?.limit ?? 1000
|
||||||
|
|
||||||
const chatState = useChatStore.getState()
|
const searchParams = new URLSearchParams({
|
||||||
const session = chatState.sessions.find((item) => item.id === sessionId)
|
offset: String(offset),
|
||||||
if (!session) return deny(404, "会话不存在")
|
limit: String(limit),
|
||||||
if (session.readonly) return deny(400, "当前会话只读")
|
})
|
||||||
if (!session.participants.some((participant) => participant.id === userId)) {
|
|
||||||
return deny(403, "仅会话参与方可发送消息")
|
return `${path}?${searchParams.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapItems<T>(value: Paginated<T> | T[]): T[] {
|
||||||
|
return Array.isArray(value) ? value : value.items
|
||||||
|
}
|
||||||
|
|
||||||
|
function denyFromError(error: unknown): ApiDecision {
|
||||||
|
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||||
|
return deny(401, "请先登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.trim()) {
|
const apiError = toApiError(error)
|
||||||
return deny(400, "消息不能为空")
|
return deny(apiError.code, apiError.msg)
|
||||||
}
|
|
||||||
|
|
||||||
chatState.sendTextMessage(sessionId, userId, content)
|
|
||||||
return allow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendImageMessage(sessionId: string, imageUrl: string) {
|
export async function listChatSessions(options?: ListChatSessionsOptions): Promise<ChatSession[]> {
|
||||||
const userId = useAuthStore.getState().user?.id
|
const res = await httpJson<Paginated<ChatSession> | ChatSession[]>(
|
||||||
if (!userId) return deny(401, "请先登录")
|
withOffsetLimit("/api/v1/chat/sessions", options),
|
||||||
|
{
|
||||||
const chatState = useChatStore.getState()
|
cache: "no-store",
|
||||||
const session = chatState.sessions.find((item) => item.id === sessionId)
|
},
|
||||||
if (!session) return deny(404, "会话不存在")
|
)
|
||||||
if (session.readonly) return deny(400, "当前会话只读")
|
return unwrapItems(res)
|
||||||
if (!session.participants.some((participant) => participant.id === userId)) {
|
}
|
||||||
return deny(403, "仅会话参与方可发送消息")
|
|
||||||
}
|
export async function getChatSessionById(sessionId: string): Promise<ChatSession | undefined> {
|
||||||
|
try {
|
||||||
if (!imageUrl.trim()) {
|
return await httpJson<ChatSession>(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}`, {
|
||||||
return deny(400, "图片地址无效")
|
cache: "no-store",
|
||||||
}
|
})
|
||||||
|
} catch (error) {
|
||||||
chatState.sendImageMessage(sessionId, userId, imageUrl)
|
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||||
return allow()
|
throw error
|
||||||
|
}
|
||||||
|
if (isApiError(error) && error.code === 404) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listChatMessages(
|
||||||
|
sessionId: string,
|
||||||
|
options?: ListChatMessagesOptions,
|
||||||
|
): Promise<ChatMessage[]> {
|
||||||
|
const res = await httpJson<Paginated<ChatMessage> | ChatMessage[]>(
|
||||||
|
withOffsetLimit(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}/messages`, options),
|
||||||
|
{
|
||||||
|
cache: "no-store",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return unwrapItems(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendTextMessage(sessionId: string, content: string): Promise<ApiDecision> {
|
||||||
|
try {
|
||||||
|
await httpJson<unknown>(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
json: { type: "text", content },
|
||||||
|
})
|
||||||
|
return allow()
|
||||||
|
} catch (error) {
|
||||||
|
return denyFromError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendImageMessage(sessionId: string, imageUrl: string): Promise<ApiDecision> {
|
||||||
|
try {
|
||||||
|
await httpJson<unknown>(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
json: { type: "image", content: imageUrl },
|
||||||
|
})
|
||||||
|
return allow()
|
||||||
|
} catch (error) {
|
||||||
|
return denyFromError(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user