small ui changes

This commit is contained in:
Saad Mohsin 2026-04-12 04:21:56 +05:00
parent 80b53535c1
commit 2235ab6770
9 changed files with 213 additions and 153 deletions

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { auth } from "@/auth.vendor"
import { prisma } from "@/lib/prisma"
import { Prisma } from "@prisma/client"
import { storeSelectPatchStorefrontAi } from "@/lib/prisma/storeSelects"
@ -547,7 +547,18 @@ export async function GET(
return NextResponse.json({ message: "Store ID is required" }, { status: 400 })
}
const store = await prisma.store.findFirst({
type StoreGetRow = {
id: string
businessName: string
categoryMetadetailsEnabled: boolean
aiModules: unknown
modulePolicies?: unknown
sharedFieldAnswers?: unknown
}
let store: StoreGetRow | null = null
try {
store = await prisma.store.findFirst({
where: { id: storeId, userId: session.user.id },
select: {
id: true,
@ -558,6 +569,21 @@ export async function GET(
sharedFieldAnswers: true,
},
})
} catch (firstErr) {
console.warn(
"[STORE_GET] full select failed (run migrations if modulePolicies/sharedFieldAnswers are missing); retrying minimal",
firstErr,
)
store = await prisma.store.findFirst({
where: { id: storeId, userId: session.user.id },
select: {
id: true,
businessName: true,
categoryMetadetailsEnabled: true,
aiModules: true,
},
})
}
if (!store) {
return NextResponse.json({ message: "Store not found" }, { status: 404 })
@ -568,8 +594,8 @@ export async function GET(
businessName: store.businessName,
categoryMetadetailsEnabled: store.categoryMetadetailsEnabled,
aiModules: store.aiModules,
modulePolicies: parseModulePoliciesJson(store.modulePolicies),
sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers),
modulePolicies: parseModulePoliciesJson(store.modulePolicies ?? null),
sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers ?? null),
})
} catch (error) {
console.error("Error reading store settings:", error)

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { auth } from "@/auth.vendor"
import { prisma } from "@/lib/prisma"
/**

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { auth } from "@/auth.vendor"
import { prisma } from "@/lib/prisma"
import { storeSwitchSchema, validationErrorResponse } from "@/lib/api-schemas"

View File

@ -6,7 +6,8 @@ import {
Flag,
Mail,
Layers,
HelpCircle
HelpCircle,
MessageCircle,
} from "lucide-react"
export default function JunoEngineLayout({
@ -28,6 +29,11 @@ export default function JunoEngineLayout({
href: "/dashboard/juno-engine/modules",
icon: Layers
},
{
label: "Storefront Chat",
href: "/dashboard/juno-engine/storefront-chat",
icon: MessageCircle
},
{
label: "Tickets",
href: "/dashboard/juno-engine/tickets",

View File

@ -128,8 +128,18 @@ export default function JunoEngineModulesPage() {
const load = useCallback(async (storeId: string) => {
const res = await fetch(`/api/stores/${encodeURIComponent(storeId)}`)
if (!res.ok) throw new Error("Failed to load store settings")
const data = (await res.json()) as {
const raw = await res.json().catch(() => ({}))
const errBody = raw as { message?: string }
if (!res.ok) {
throw new Error(
typeof errBody.message === "string" && errBody.message.trim()
? errBody.message
: res.status === 401
? "Sign in again to load store settings."
: "Failed to load store settings",
)
}
const data = raw as {
aiModules?: unknown
sharedFieldAnswers?: unknown
categoryMetadetailsEnabled?: boolean
@ -852,7 +862,7 @@ export default function JunoEngineModulesPage() {
>
Widget appearance:{" "}
<Link
href="/dashboard/storefront-chat"
href="/dashboard/juno-engine/storefront-chat"
className="font-semibold text-indigo-500 hover:underline"
>
Storefront Chat Inbox

View File

@ -0,0 +1,5 @@
import StorefrontChatInbox from "@/components/storefront/StorefrontChatInbox"
export default function JunoEngineStorefrontChatPage() {
return <StorefrontChatInbox />
}

View File

@ -1,136 +1,6 @@
"use server"
import { prisma } from "@/lib/prisma"
import { storeSelectDashboardStorefrontInbox } from "@/lib/prisma/storeSelects"
import { auth } from "@/auth.vendor"
import { redirect } from "next/navigation"
import Link from "next/link"
export default async function StorefrontChatInboxPage() {
const session = await auth()
if (!session?.user?.id) {
redirect("/login")
/** @deprecated Use JUNO Engine → Storefront Chat */
export default function StorefrontChatRedirectPage() {
redirect("/dashboard/juno-engine/storefront-chat")
}
const stores = await prisma.store.findMany({
where: { userId: session.user.id, onboardingComplete: true },
select: storeSelectDashboardStorefrontInbox,
})
const store = stores.find((s) => s.isActive) ?? stores[0]
if (!store) {
redirect("/onboarding")
}
const conversations = await prisma.storefrontChatConversation.findMany({
where: { storeId: store.id },
orderBy: { updatedAt: "desc" },
take: 25,
select: { id: true, visitorId: true, customerEmail: true, updatedAt: true }
})
const conversationIds = conversations.map((c: { id: string }) => c.id)
const messages = await prisma.storefrontChatMessage.findMany({
where: conversationIds.length ? { conversationId: { in: conversationIds } } : undefined,
orderBy: { createdAt: "desc" },
take: 200,
select: {
id: true,
content: true,
createdAt: true,
senderType: true,
conversation: {
select: {
visitorId: true,
customerEmail: true
}
}
}
})
const sorted = [...messages].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
const aiCount = sorted.filter((m) => m.senderType === "AI").length
const customerCount = sorted.filter((m) => m.senderType === "CUSTOMER").length
return (
<div className="flex flex-col gap-4 p-1">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<h1 className="text-xl font-black text-white">Storefront Chat Inbox</h1>
<p className="text-sm text-slate-400 mt-1 truncate">
{store.businessName} · {store.shopifyDomain}
</p>
</div>
<div className="shrink-0 text-right">
<p className="text-sm font-black text-white">{conversations.length} visitors</p>
<p className="text-xs text-slate-500 mt-1">{sorted.length} messages</p>
<p className="text-[11px] text-slate-500 mt-1">
{customerCount} customer · {aiCount} AI
</p>
</div>
</div>
<p className="text-sm text-slate-400">
L1 AI modules and catalog metafields:{" "}
<Link
href="/dashboard/juno-engine/modules"
className="font-semibold text-indigo-400 hover:underline"
>
JUNO Engine → Modules
</Link>
.
</p>
{sorted.length === 0 ? (
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-slate-300">
<p className="font-black">No storefront chat messages yet.</p>
<p className="text-sm text-slate-400 mt-2">When customers send a message from the widget, it will show here.</p>
</div>
) : (
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<p className="text-sm font-black text-white">Recent activity</p>
<p className="text-xs text-slate-400">Read-only</p>
</div>
<div className="max-h-[70vh] overflow-y-auto p-4 space-y-3">
{sorted.map((m) => (
<div key={m.id} className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<span
className={`text-[10px] font-black uppercase tracking-widest rounded-full px-2 py-1 border ${
m.senderType === "AI"
? "text-indigo-300 border-indigo-400/30 bg-indigo-500/10"
: "text-emerald-300 border-emerald-400/30 bg-emerald-500/10"
}`}
>
{m.senderType === "CUSTOMER" ? "CUSTOMER" : "AI"}
</span>
<p className="text-xs font-black uppercase tracking-widest text-slate-400 truncate">
{m.conversation?.visitorId || "visitor"}
</p>
</div>
{m.conversation?.customerEmail ? (
<p className="text-[11px] text-slate-500 truncate">{m.conversation.customerEmail}</p>
) : null}
</div>
<p className="text-[11px] text-slate-500 shrink-0">
{new Date(m.createdAt).toLocaleString()}
</p>
</div>
<div className="whitespace-pre-wrap text-sm text-white/90 bg-black/20 border border-white/10 rounded-2xl px-4 py-3">
{m.content}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}

View File

@ -16,6 +16,7 @@ import {
Mail,
Layers,
HelpCircle,
MessageCircle,
Store
} from "lucide-react"
import { cn } from "@/lib/utils"
@ -77,6 +78,11 @@ const routes = [
subRoutes: [
{ label: "Knowledge Base", href: "/dashboard/juno-engine/knowledge-base", icon: HelpCircle },
{ label: "Modules", href: "/dashboard/juno-engine/modules", icon: Layers },
{
label: "Storefront Chat",
href: "/dashboard/juno-engine/storefront-chat",
icon: MessageCircle,
},
{ label: "Tickets", href: "/dashboard/juno-engine/tickets", icon: Ticket },
{ label: "Flagged Emails", href: "/dashboard/juno-engine/flagged-email", icon: Flag },
{ label: "Gmail", href: "/dashboard/juno-engine/gmail-connect", icon: Mail },

View File

@ -0,0 +1,137 @@
import { prisma } from "@/lib/prisma"
import { storeSelectDashboardStorefrontInbox } from "@/lib/prisma/storeSelects"
import { auth } from "@/auth.vendor"
import { redirect } from "next/navigation"
import Link from "next/link"
export default async function StorefrontChatInbox() {
const session = await auth()
if (!session?.user?.id) {
redirect("/login")
}
const stores = await prisma.store.findMany({
where: { userId: session.user.id, onboardingComplete: true },
select: storeSelectDashboardStorefrontInbox,
})
const store = stores.find((s) => s.isActive) ?? stores[0]
if (!store) {
redirect("/onboarding")
}
const conversations = await prisma.storefrontChatConversation.findMany({
where: { storeId: store.id },
orderBy: { updatedAt: "desc" },
take: 25,
select: { id: true, visitorId: true, customerEmail: true, updatedAt: true },
})
const conversationIds = conversations.map((c) => c.id)
const messages = await prisma.storefrontChatMessage.findMany({
where: conversationIds.length ? { conversationId: { in: conversationIds } } : undefined,
orderBy: { createdAt: "desc" },
take: 200,
select: {
id: true,
content: true,
createdAt: true,
senderType: true,
conversation: {
select: {
visitorId: true,
customerEmail: true,
},
},
},
})
const sorted = [...messages].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
const aiCount = sorted.filter((m) => m.senderType === "AI").length
const customerCount = sorted.filter((m) => m.senderType === "CUSTOMER").length
return (
<div className="flex flex-col gap-4 p-1">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<h1 className="text-xl font-black text-white">Storefront Chat Inbox</h1>
<p className="text-sm text-slate-400 mt-1 truncate">
{store.businessName} · {store.shopifyDomain}
</p>
</div>
<div className="shrink-0 text-right">
<p className="text-sm font-black text-white">{conversations.length} visitors</p>
<p className="text-xs text-slate-500 mt-1">{sorted.length} messages</p>
<p className="text-[11px] text-slate-500 mt-1">
{customerCount} customer · {aiCount} AI
</p>
</div>
</div>
<p className="text-sm text-slate-400">
L1 AI modules and catalog metafields:{" "}
<Link
href="/dashboard/juno-engine/modules"
className="font-semibold text-indigo-400 hover:underline"
>
JUNO Engine → Modules
</Link>
.
</p>
{sorted.length === 0 ? (
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 text-slate-300">
<p className="font-black">No storefront chat messages yet.</p>
<p className="text-sm text-slate-400 mt-2">
When customers send a message from the widget, it will show here.
</p>
</div>
) : (
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<p className="text-sm font-black text-white">Recent activity</p>
<p className="text-xs text-slate-400">Read-only</p>
</div>
<div className="max-h-[70vh] overflow-y-auto p-4 space-y-3">
{sorted.map((m) => (
<div key={m.id} className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<span
className={`text-[10px] font-black uppercase tracking-widest rounded-full px-2 py-1 border ${
m.senderType === "AI"
? "text-indigo-300 border-indigo-400/30 bg-indigo-500/10"
: "text-emerald-300 border-emerald-400/30 bg-emerald-500/10"
}`}
>
{m.senderType === "CUSTOMER" ? "CUSTOMER" : "AI"}
</span>
<p className="text-xs font-black uppercase tracking-widest text-slate-400 truncate">
{m.conversation?.visitorId || "visitor"}
</p>
</div>
{m.conversation?.customerEmail ? (
<p className="text-[11px] text-slate-500 truncate">
{m.conversation.customerEmail}
</p>
) : null}
</div>
<p className="text-[11px] text-slate-500 shrink-0">
{new Date(m.createdAt).toLocaleString()}
</p>
</div>
<div className="whitespace-pre-wrap text-sm text-white/90 bg-black/20 border border-white/10 rounded-2xl px-4 py-3">
{m.content}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}