Skip to content

Commit 7d0be11

Browse files
authored
feat: add shared chat screen (#75)
1 parent 0b82e4d commit 7d0be11

File tree

4 files changed

+227
-4
lines changed

4 files changed

+227
-4
lines changed
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { SourcesList } from "@/app/components/chat/sources-list"
2+
import type { Tables } from "@/app/types/database.types"
3+
import { Message, MessageContent } from "@/components/prompt-kit/message"
4+
import { Button } from "@/components/ui/button"
5+
import { cn } from "@/lib/utils"
6+
import type { Message as MessageAISDK } from "@ai-sdk/react"
7+
import { ArrowUpRight } from "@phosphor-icons/react/dist/ssr"
8+
import Link from "next/link"
9+
import { Header } from "./header"
10+
11+
type MessageType = Tables<"messages">
12+
13+
type ArticleProps = {
14+
date: string
15+
category: string
16+
title: string
17+
subtitle: string
18+
cta: {
19+
text: string
20+
href: string
21+
}
22+
messages: MessageType[]
23+
}
24+
25+
export default function Article({
26+
date,
27+
category,
28+
title,
29+
subtitle,
30+
cta,
31+
messages,
32+
}: ArticleProps) {
33+
return (
34+
<>
35+
<Header />
36+
<div className="mx-auto max-w-3xl px-4 py-12 md:py-24">
37+
<div className="mb-8 flex items-center justify-center gap-2 text-sm font-medium">
38+
<time
39+
dateTime={new Date(date).toISOString().split("T")[0]}
40+
className="text-foreground"
41+
>
42+
{new Date(date).toLocaleDateString("en-US", {
43+
year: "numeric",
44+
month: "long",
45+
day: "numeric",
46+
})}
47+
</time>
48+
<span className="mx-2">·</span>
49+
<span className="text-muted-foreground hover:text-foreground transition-colors">
50+
<Link href={`/agents/${category}`}>{category}</Link>
51+
</span>
52+
</div>
53+
54+
<h1 className="mb-4 text-center text-4xl font-medium tracking-tight md:text-5xl">
55+
{title}
56+
</h1>
57+
58+
<p className="text-foreground mb-8 text-center text-lg">{subtitle}</p>
59+
60+
<div className="fixed bottom-6 left-0 z-50 flex w-full justify-center">
61+
<Link href="/agents">
62+
<Button
63+
variant="outline"
64+
className="text-muted-foreground group flex h-12 w-full max-w-36 items-center justify-between rounded-full py-2 pr-2 pl-4 shadow-sm transition-all hover:scale-[1.02] active:scale-[0.98]"
65+
>
66+
Ask Zola{" "}
67+
<div className="rounded-full bg-black/20 p-2 backdrop-blur-sm transition-colors group-hover:bg-black/30">
68+
<ArrowUpRight className="h-4 w-4 text-white" />
69+
</div>
70+
</Button>
71+
</Link>
72+
</div>
73+
<div className="mt-20 w-full">
74+
{messages.map((message) => {
75+
const parts = message?.parts as MessageAISDK["parts"]
76+
const sources = parts
77+
?.filter((part) => part?.type === "source")
78+
.map((part) => part?.source)
79+
80+
return (
81+
<div key={message.id}>
82+
<Message
83+
key={message.id}
84+
className={cn(
85+
"mb-4 flex flex-col gap-0",
86+
message.role === "assistant" && "w-full items-start",
87+
message.role === "user" && "w-full items-end"
88+
)}
89+
>
90+
<MessageContent
91+
markdown={true}
92+
className={cn(
93+
message.role === "user" && "bg-blue-600 text-white",
94+
message.role === "assistant" &&
95+
"w-full min-w-full bg-transparent",
96+
"prose-h1:scroll-m-20 prose-h1:text-2xl prose-h1:font-semibold prose-h2:mt-8 prose-h2:scroll-m-20 prose-h2:text-xl prose-h2:mb-3 prose-h2:font-medium prose-h3:scroll-m-20 prose-h3:text-base prose-h3:font-medium prose-h4:scroll-m-20 prose-h5:scroll-m-20 prose-h6:scroll-m-20 prose-strong:font-medium prose-table:block prose-table:overflow-y-auto"
97+
)}
98+
>
99+
{message.content}
100+
</MessageContent>
101+
</Message>
102+
{sources && <SourcesList sources={sources} />}
103+
</div>
104+
)
105+
})}
106+
</div>
107+
</div>
108+
</>
109+
)
110+
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Link from "next/link"
2+
import React from "react"
3+
4+
export function Header() {
5+
return (
6+
<header className="h-app-header fixed top-0 right-0 left-0 z-50">
7+
<div className="h-app-header top-app-header bg-background pointer-events-none absolute left-0 z-50 mx-auto w-full to-transparent backdrop-blur-xl [-webkit-mask-image:linear-gradient(to_bottom,black,transparent)] lg:hidden"></div>
8+
<div className="bg-background relative mx-auto flex h-full max-w-6xl items-center justify-between px-4 sm:px-6 lg:bg-transparent lg:px-8">
9+
<Link href="/" className="text-xl font-medium tracking-tight">
10+
Zola
11+
</Link>
12+
</div>
13+
</header>
14+
)
15+
}
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { APP_DOMAIN } from "@/lib/config"
2+
import { createClient } from "@/lib/supabase/server"
3+
import type { Metadata } from "next"
4+
import { redirect } from "next/navigation"
5+
import Article from "./article"
6+
7+
export const dynamic = "force-static"
8+
9+
export async function generateMetadata({
10+
params,
11+
}: {
12+
params: Promise<{ agentSlug: string; chatId: string }>
13+
}): Promise<Metadata> {
14+
const { agentSlug, chatId } = await params
15+
const supabase = await createClient()
16+
17+
const { data: chat } = await supabase
18+
.from("chats")
19+
.select("title, system_prompt")
20+
.eq("id", chatId)
21+
.single()
22+
23+
const { data: agent } = await supabase
24+
.from("agents")
25+
.select("name")
26+
.eq("slug", agentSlug)
27+
.single()
28+
29+
const title = chat?.title || `Public Zola research`
30+
const description =
31+
chat?.system_prompt || `A research powered by ${agent?.name}`
32+
33+
return {
34+
title,
35+
description,
36+
openGraph: {
37+
title,
38+
description,
39+
type: "article",
40+
url: `${APP_DOMAIN}/agents/${agentSlug}/${chatId}`,
41+
},
42+
twitter: {
43+
card: "summary_large_image",
44+
title,
45+
description,
46+
},
47+
}
48+
}
49+
50+
export default async function AgentChat({
51+
params,
52+
}: {
53+
params: Promise<{ agentSlug: string; chatId: string }>
54+
}) {
55+
const { agentSlug, chatId } = await params
56+
const supabase = await createClient()
57+
58+
const { data: chatData, error: chatError } = await supabase
59+
.from("chats")
60+
.select("id, title, model, system_prompt, agent_id, created_at")
61+
.eq("id", chatId)
62+
.single()
63+
64+
if (chatError || !chatData) {
65+
redirect("/agents")
66+
}
67+
68+
const { data: messagesData, error: messagesError } = await supabase
69+
.from("messages")
70+
.select("*")
71+
.eq("chat_id", chatId)
72+
.order("created_at", { ascending: true })
73+
74+
if (messagesError || !messagesData) {
75+
redirect("/agents")
76+
}
77+
78+
const { data: agentData, error: agentError } = await supabase
79+
.from("agents")
80+
.select("*")
81+
.eq("slug", agentSlug)
82+
.single()
83+
84+
if (agentError || !agentData) {
85+
redirect("/agents")
86+
}
87+
88+
return (
89+
<Article
90+
messages={messagesData}
91+
date={chatData.created_at || ""}
92+
category={agentData.name}
93+
title={chatData.title || ""}
94+
subtitle={`A conversation with ${agentData.name}, an AI agent built in Zola`}
95+
cta={{
96+
text: "Read more",
97+
href: "/agents",
98+
}}
99+
/>
100+
)
101+
}

components/prompt-kit/message.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import {
77
import { cn } from "@/lib/utils"
88
import dynamic from "next/dynamic"
99

10-
const Markdown = dynamic(
11-
() => import("./markdown").then((mod) => mod.Markdown),
12-
{ ssr: false }
13-
)
10+
const Markdown = dynamic(() => import("./markdown").then((mod) => mod.Markdown))
1411

1512
export type MessageProps = {
1613
children: React.ReactNode

0 commit comments

Comments
 (0)