Skip to content

Commit

Permalink
🥕 v0.17.0-b20 🌱 ThreeD: Home Design OpenAI
Browse files Browse the repository at this point in the history
  • Loading branch information
marty-mcgee committed Jan 9, 2025
1 parent a089c33 commit d7a7365
Show file tree
Hide file tree
Showing 74 changed files with 4,946 additions and 3 deletions.
27 changes: 25 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "threed-garden",
"version": "0.17.0-beta.19",
"version": "0.17.0-beta.20",
"description": "ThreeD Garden: 🥕 ThreeD Garden: WebGL 3D Environment Interface for Next.JS React TypeScript Three.JS React-Three Physics, 2D Paper.JS, APIs: Apollo GraphQL, WordPress, CSS: Tailwind, Radix-UI, Libraries: FarmBot React 3D, Open.AI",
"author": "Marty McGee <mcgee.marty@gmail.com> (https://github.com/marty-mcgee)",
"license": "MIT",
Expand Down Expand Up @@ -127,7 +127,30 @@
"chroma-js": "3.1.2",
"swr": "2.2.5",

"use-count-up": "3.0.1"
"use-count-up": "3.0.1",


"ai": "4.0.31",
"openai": "4.77.4",
"zod": "3.23.8",
"@ai-sdk/openai": "1.0.16",

"@vercel/analytics": "1.4.1",
"@vercel/kv": "3.0.0",

"sonner": "1.7.1",
"react-markdown": "8.0.7",
"framer-motion": "11.3.30",
"geist": "1.3.1",
"nanoid": "5.0.7",
"react-intersection-observer": "9.10.3",
"react-syntax-highlighter": "15.5.0",
"react-textarea-autosize": "8.5.3",
"remark-gfm": "3.0.1",
"remark-math": "5.1.1",

"attr-accept": "2.2.4",
"react-dropzone": "14.2.9"

},

Expand Down
65 changes: 65 additions & 0 deletions src/app/ai/(chat)/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { type Metadata } from 'next'
import { notFound, redirect } from 'next/navigation'

import { auth } from '@/auth'
import { getChat, getMissingKeys } from '@/app/actions'
import { Chat } from '~/src/lib/ai/chat'
import { AI } from '~/src/lib/threed/threed.ai/chat/actions'
import { Session } from '@/lib/types'

export interface ChatPageProps {
params: {
id: string
}
}

export async function generateMetadata({
params
}: ChatPageProps): Promise<Metadata> {
const session = await auth()

if (!session?.user) {
return {}
}

const chat = await getChat(params.id, session.user.id)

if (!chat || 'error' in chat) {
redirect('/')
} else {
return {
title: chat?.title.toString().slice(0, 50) ?? 'Chat'
}
}
}

export default async function ChatPage({ params }: ChatPageProps) {
const session = (await auth()) as Session
const missingKeys = await getMissingKeys()

if (!session?.user) {
redirect(`/login?next=/chat/${params.id}`)
}

const userId = session.user.id as string
const chat = await getChat(params.id, userId)

if (!chat || 'error' in chat) {
redirect('/')
} else {
if (chat?.userId !== session?.user?.id) {
notFound()
}

return (
<AI initialAIState={{ chatId: chat.id, messages: chat.messages }}>
<Chat
id={chat.id}
session={session}
initialMessages={chat.messages}
missingKeys={missingKeys}
/>
</AI>
)
}
}
14 changes: 14 additions & 0 deletions src/app/ai/(chat)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SidebarDesktop } from '~/src/lib/ai/sidebar-desktop'

interface ChatLayoutProps {
children: React.ReactNode
}

export default async function ChatLayout({ children }: ChatLayoutProps) {
return (
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
<SidebarDesktop />
{children}
</div>
)
}
22 changes: 22 additions & 0 deletions src/app/ai/(chat)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { nanoid } from '@/lib/utils'
import { Chat } from '~/src/lib/ai/chat'
import { AI } from '~/src/lib/threed/threed.ai/chat/actions'
import { auth } from '@/auth'
import { Session } from '@/lib/types'
import { getMissingKeys } from '@/app/actions'

export const metadata = {
title: 'Next.js AI Chatbot'
}

export default async function IndexPage() {
const id = nanoid()
const session = (await auth()) as Session
const missingKeys = await getMissingKeys()

return (
<AI initialAIState={{ chatId: id, messages: [] }}>
<Chat id={id} session={session} missingKeys={missingKeys} />
</AI>
)
}
172 changes: 172 additions & 0 deletions src/app/ai/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { kv } from '@vercel/kv'

import { auth } from '@/auth'
import { type Chat } from '@/lib/types'

export async function getChats(userId?: string | null) {
const session = await auth()

if (!userId) {
return []
}

if (userId !== session?.user?.id) {
return {
error: 'Unauthorized'
}
}

try {
const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
rev: true
})

for (const chat of chats) {
pipeline.hgetall(chat)
}

const results = await pipeline.exec()

return results as Chat[]
} catch (error) {
return []
}
}

export async function getChat(id: string, userId: string) {
const session = await auth()

if (userId !== session?.user?.id) {
return {
error: 'Unauthorized'
}
}

const chat = await kv.hgetall<Chat>(`chat:${id}`)

if (!chat || (userId && chat.userId !== userId)) {
return null
}

return chat
}

export async function removeChat({ id, path }: { id: string; path: string }) {
const session = await auth()

if (!session) {
return {
error: 'Unauthorized'
}
}

// Convert uid to string for consistent comparison with session.user.id
const uid = String(await kv.hget(`chat:${id}`, 'userId'))

if (uid !== session?.user?.id) {
return {
error: 'Unauthorized'
}
}

await kv.del(`chat:${id}`)
await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)

revalidatePath('/')
return revalidatePath(path)
}

export async function clearChats() {
const session = await auth()

if (!session?.user?.id) {
return {
error: 'Unauthorized'
}
}

const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
if (!chats.length) {
return redirect('/')
}
const pipeline = kv.pipeline()

for (const chat of chats) {
pipeline.del(chat)
pipeline.zrem(`user:chat:${session.user.id}`, chat)
}

await pipeline.exec()

revalidatePath('/')
return redirect('/')
}

export async function getSharedChat(id: string) {
const chat = await kv.hgetall<Chat>(`chat:${id}`)

if (!chat || !chat.sharePath) {
return null
}

return chat
}

export async function shareChat(id: string) {
const session = await auth()

if (!session?.user?.id) {
return {
error: 'Unauthorized'
}
}

const chat = await kv.hgetall<Chat>(`chat:${id}`)

if (!chat || chat.userId !== session.user.id) {
return {
error: 'Something went wrong'
}
}

const payload = {
...chat,
sharePath: `/share/${chat.id}`
}

await kv.hmset(`chat:${chat.id}`, payload)

return payload
}

export async function saveChat(chat: Chat) {
const session = await auth()

if (session && session.user) {
const pipeline = kv.pipeline()
pipeline.hmset(`chat:${chat.id}`, chat)
pipeline.zadd(`user:chat:${chat.userId}`, {
score: Date.now(),
member: `chat:${chat.id}`
})
await pipeline.exec()
} else {
return
}
}

export async function refreshHistory(path: string) {
redirect(path)
}

export async function getMissingKeys() {
const keysRequired = ['OPENAI_API_KEY']
return keysRequired
.map(key => (process.env[key] ? '' : key))
.filter(key => key !== '')
}
76 changes: 76 additions & 0 deletions src/app/ai/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;

--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;

--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;

--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;

--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;

--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;

--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;

--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;

--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;

--radius: 0.5rem;
}

.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;

--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;

--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;

--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;

--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;

--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;

--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;

--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;

--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
Loading

0 comments on commit d7a7365

Please sign in to comment.