diff --git a/biome.json b/biome.json index 7dc6079..b6bc16c 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "files": { - "ignore": ["playground/**"], + "ignore": ["playground/**", "**/payload-types.ts"], "ignoreUnknown": true }, "vcs": { diff --git a/packages/chat/package.json b/packages/chat/package.json index a9868a0..a19e664 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -26,6 +26,7 @@ "build:ui": "vite build", "build:plugin": "unbuild --sourcemaps", "build": "pnpm build:plugin && pnpm build:ui", + "generate": "payload generate:types", "tailwind": "npx tailwindcss -i ./src/ui/tailwind.css -o ./dist/ui/tailwind.css" }, "dependencies": { diff --git a/packages/chat/src/collections/createMessageCollection.ts b/packages/chat/src/collections/createMessageCollection.ts index c93196d..187f540 100644 --- a/packages/chat/src/collections/createMessageCollection.ts +++ b/packages/chat/src/collections/createMessageCollection.ts @@ -1,7 +1,14 @@ import type {ChatPluginConfig} from '../plugin/types' -import type {ArrayField, CollectionConfig} from 'payload' +import type {ArrayField, CollectionConfig, FieldHook} from 'payload' import {get} from 'radash' +const setSentDate: FieldHook< + { id: string }, + Array<{ sent: Date; id: string }> +> = ({ data, value, operation }) => { + return (value ?? []).map((m) => ({ ...m, sent: m.sent ?? new Date() })) +} + export const createMessageCollection = ( config: ChatPluginConfig['generatedCollections']['chats'], ): CollectionConfig => { @@ -10,18 +17,33 @@ export const createMessageCollection = ( const messagesField: ArrayField = { name: 'messages', type: 'array', + required: true, + defaultValue: [], + hooks: { + beforeChange: [setSentDate], + }, fields: [ - { name: 'sent', type: 'date', required: true }, - { name: 'text', type: 'text', required: true }, { - name: 'role', - type: 'select', - options: ['assistant', 'user'], - required: true, + type: 'row', + fields: [ + { + name: 'sent', + type: 'date', + admin: { date: { pickerAppearance: 'dayAndTime' }, readOnly: true }, + }, + { + name: 'role', + type: 'select', + options: ['assistant', 'user'], + required: true, + }, + ], }, + { name: 'text', type: 'text', required: true }, { - name: 'context', - type: 'group', + label: 'context', + type: 'collapsible', + admin: { initCollapsed: true }, fields: [ { name: 'collection', type: 'text' }, { name: 'view', type: 'select', options: ['list', 'record'] }, @@ -31,19 +53,29 @@ export const createMessageCollection = ( } return { + access: { + read: ({ req, data }) => { + if (!req.user) return false + if (!data) return true + return { + 'user.email': { equals: req.user.email }, + } + }, + }, fields: [ messagesField, { name: 'user', type: 'relationship', - relationTo: ['users'], + relationTo: 'users', + hasMany: false, required: true, }, { - name:'description', - type:'text', - defaultValue:'A Recent Chat' - } + name: 'description', + type: 'text', + defaultValue: 'A Recent Chat', + }, ], slug, } diff --git a/packages/chat/src/payload-types.ts b/packages/chat/src/payload-types.ts new file mode 100644 index 0000000..73a5963 --- /dev/null +++ b/packages/chat/src/payload-types.ts @@ -0,0 +1,146 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +export interface Config { + auth: { + users: UserAuthOperations; + }; + collections: { + chats: Chat; + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + db: { + defaultIDType: string; + }; + globals: {}; + locale: null; + user: User & { + collection: 'users'; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "chats". + */ +export interface Chat { + messages: { + sent?: string | null; + role: 'assistant' | 'user'; + text: string; + collection?: string | null; + view?: ('list' | 'record') | null; + id?: string | null; + }[]; + user: number | User; + description?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users". + */ +export interface User { + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + password?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + document?: + | ({ + relationTo: 'chats'; + value: number | Chat; + } | null) + | ({ + relationTo: 'users'; + value: number | User; + } | null); + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: number | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences". + */ +export interface PayloadPreference { + user: { + relationTo: 'users'; + value: number | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations". + */ +export interface PayloadMigration { + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + export interface GeneratedTypes extends Config {} +} \ No newline at end of file diff --git a/packages/chat/src/payload.config.ts b/packages/chat/src/payload.config.ts new file mode 100644 index 0000000..69634c6 --- /dev/null +++ b/packages/chat/src/payload.config.ts @@ -0,0 +1,17 @@ +import path from "node:path"; +import {fileURLToPath} from "node:url"; +import type {Config} from 'payload' +import {buildConfig} from 'payload' +import {chatPlugin} from "./plugin/ChatPlugin"; +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export default buildConfig({ + collections: [], + db: {} as Config['db'], + secret: 'secret', + typescript: { + outputFile: path.resolve(dirname, 'payload-types.ts'), + }, + plugins:[chatPlugin({})] +}) diff --git a/packages/chat/src/payload/getUser.ts b/packages/chat/src/payload/getUser.ts new file mode 100644 index 0000000..bded8ac --- /dev/null +++ b/packages/chat/src/payload/getUser.ts @@ -0,0 +1,9 @@ +import {cookies, headers} from "next/headers"; +import payload, {extractJWT} from "payload"; + +export const getUser =()=>{ + + const jwt = extractJWT({headers: headers(), payload:payload, isGraphQL:false}) + + +} \ No newline at end of file diff --git a/packages/chat/src/plugin/ChatPlugin.ts b/packages/chat/src/plugin/ChatPlugin.ts index 7a47e67..73076f8 100644 --- a/packages/chat/src/plugin/ChatPlugin.ts +++ b/packages/chat/src/plugin/ChatPlugin.ts @@ -36,21 +36,19 @@ export const chatPlugin = components: { views: { chat: { - exact:true, - sensitive:false, - path:'/chat', + exact: false, + sensitive: false, + path: '/chat/:chat_id', Component: '@payload-llm-plugins/chat/ChatView', }, }, }, }, collections: [ - createMessageCollection( - pluginConfig.generatedCollections.chats, - ), + createMessageCollection(pluginConfig.generatedCollections.chats), ], } satisfies Partial) - console.log(updatedConfig.admin.components.views.chat) + const clientConfig = pluginConfig.modelClientConfig // @ts-ignore diff --git a/packages/chat/src/ui/chat/Input.client.tsx b/packages/chat/src/ui/chat/Input.client.tsx new file mode 100644 index 0000000..c3408b3 --- /dev/null +++ b/packages/chat/src/ui/chat/Input.client.tsx @@ -0,0 +1,27 @@ +'use client' +import * as React from 'react' +import {insertMessageAction} from './insertMessage.action' + +interface ChatId { + chatId?: string | number +} + +export const SendMessageUI = ({ chatId }: ChatId) => { + return ( +
+ + + + +
+ ) +} diff --git a/packages/chat/src/ui/chat/Message.tsx b/packages/chat/src/ui/chat/Message.tsx index a22fc86..cdd42b2 100644 --- a/packages/chat/src/ui/chat/Message.tsx +++ b/packages/chat/src/ui/chat/Message.tsx @@ -1,10 +1,17 @@ import * as React from 'react' -import type {Message} from './types' +import type {Chat} from '../../payload-types' -export const MessageElement = ({ message }: { message: Message }) => { +export const MessageElement = ({ + message, +}: { message: Chat['messages'][number] }) => { return ( -
-
+
+
{message.text}
diff --git a/packages/chat/src/ui/chat/index.tsx b/packages/chat/src/ui/chat/index.tsx index 6e5fe61..e02736e 100644 --- a/packages/chat/src/ui/chat/index.tsx +++ b/packages/chat/src/ui/chat/index.tsx @@ -1,29 +1,26 @@ -import * as React from "react"; +import * as React from 'react' import {MessageElement} from './Message' -import type {Message} from './types' +import type {PayloadRequest} from 'payload' +import {getChat} from '../hooks/getConversation' +import {SendMessageUI} from './Input.client' interface ChatProps { - // biome-ignore lint/suspicious/noConfusingVoidType: - chat:Promise<{messages: Message[]}|void> + req: PayloadRequest + chatId: string | number | undefined } -export const ChatUI = ({ chat }: ChatProps) => { - +export const ChatUI = ({ req, chatId }: ChatProps) => { + const chat = getChat(req, chatId) const messages = React.use(chat) - console.log(messages) return (
{messages?.messages?.map((message) => ( - + ))}
-
- - - -
+
) diff --git a/packages/chat/src/ui/chat/insertMessage.action.ts b/packages/chat/src/ui/chat/insertMessage.action.ts new file mode 100644 index 0000000..6571696 --- /dev/null +++ b/packages/chat/src/ui/chat/insertMessage.action.ts @@ -0,0 +1,42 @@ +import {getPayload} from 'payload' +import type {Chat, User} from '../../payload-types' +import {headers} from 'next/headers' +import config from '@payload-config' +import {get, isString} from 'radash' + +interface Props { + message: string + chatId?: string | number + user: User +} + +export async function insertMessageAction(data: FormData) { + 'use server' + + const instance = await getPayload({ config }) + const auth = await instance.auth({ headers: headers() }) + + const user = auth.user + + const message = data.get('message') + if (!user || !isString(message)) return + const chatId = data.get('chatId')?.toString() + + const chat = + chatId && + (await instance.findByID({ collection: 'chats', id: chatId, depth: 0 })) + if (chat) { + chat.messages.push({ text: message, role: 'user' }) + await instance.update({ + collection: 'chats', + data: chat, + id: chatId, + }) + } else { + const chat = { + messages: [{ role: 'user', text: message }], + user: get(user, 'id'), + } satisfies Partial + await instance.create({ collection: 'chats', data: chat, user }) + } +} diff --git a/packages/chat/src/ui/chat/types.ts b/packages/chat/src/ui/chat/types.ts deleted file mode 100644 index 7c71943..0000000 --- a/packages/chat/src/ui/chat/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Message { - role: 'assistant' | 'user' - text: string -} \ No newline at end of file diff --git a/packages/chat/src/ui/hooks/getConversation.ts b/packages/chat/src/ui/hooks/getConversation.ts new file mode 100644 index 0000000..c87028a --- /dev/null +++ b/packages/chat/src/ui/hooks/getConversation.ts @@ -0,0 +1,28 @@ +import type {PayloadRequest} from 'payload' +import type {Chat} from '../../payload-types' +import {isObject} from 'radash' + +export const getChat = async ( + req: PayloadRequest, + id: string | number | undefined | null, +): Promise> => { + if (!id) { + return Promise.resolve({ messages: [] }) + } + + const conversation = await req.payload.findByID({ + overrideAccess: false, + user: req.user, + id, + depth: 2, + collection: 'chats', + }) + if ( + conversation && + isObject(conversation.user) && + conversation.user.email === req.user?.email + ) { + return conversation + } + return { messages: [] } +} diff --git a/packages/chat/src/ui/hooks/queryConversations.ts b/packages/chat/src/ui/hooks/queryConversations.ts new file mode 100644 index 0000000..9c71b6c --- /dev/null +++ b/packages/chat/src/ui/hooks/queryConversations.ts @@ -0,0 +1,14 @@ +import type {PayloadRequest} from 'payload' +import type {Chat} from "../../payload-types"; + +export const queryChats = async (req: PayloadRequest):Promise => { + const records = await req.payload.find({ + overrideAccess: false, + user: req.user, + collection: 'chats', + where: { "user.email": { equals: req.user?.email } }, + sort: '-sent', + }) + + return records.docs +} diff --git a/packages/chat/src/ui/page/ChatView.tsx b/packages/chat/src/ui/page/ChatView.tsx index ef34573..389ed06 100644 --- a/packages/chat/src/ui/page/ChatView.tsx +++ b/packages/chat/src/ui/page/ChatView.tsx @@ -1,25 +1,21 @@ import '../tailwind.css' - -import type {AdminViewProps} from 'payload' -//@ts-ignore +import type {AdminViewProps} from 'payload' //@ts-ignore // biome-ignore lint/style/useImportType: -import * as React from 'react' -import {ReactNode, Suspense} from 'react' -import {DefaultTemplate} from '@payloadcms/next/templates' +import * as React from 'react' +import {type ReactNode, Suspense} from 'react' import {redirect} from 'next/navigation' import {ChatUI} from '../chat' import {ConversationsList} from './ConversationsList' +import {getParamValue} from './parseParam' +import {get} from "radash"; const ChatView: React.FC = ({ initPageResult, params, - searchParams, }: AdminViewProps): ReactNode => { - const { permissions, req: { - payload, payload: { config: { routes: { admin: adminRoute }, @@ -27,38 +23,15 @@ const ChatView: React.FC = ({ }, user, }, - visibleEntities, } = initPageResult if (!user || (user && !permissions?.canAccessAdmin)) { return redirect(`${adminRoute}/unauthorized`) } - const conversations = payload - .find({ collection: 'chats', where: { description:{exists:true} } }) - .then((chat) => { - return chat.docs.map((c) => ({ - id: c.id.toString(), - description: c.description, - })) - }).catch(console.error) - - const chatId = 1 - const chat = chatId - ? payload - .find({ - collection: 'chats', - where: { id: { equals: chatId } }, - }) - .then((chats) => { - console.log(chats) - const chat = chats.docs[0] - console.log(chat) + const chatId = get(params,'segments[1]', undefined) + console.log(chatId) - if (!chat) return { messages: [] } - return { ...chat, messages: chat.messages } - }).catch(console.error) - : Promise.resolve({ messages: [] }) return ( <> @@ -69,12 +42,12 @@ const ChatView: React.FC = ({
Loading

}> - +
@@ -83,4 +56,4 @@ const ChatView: React.FC = ({ ) } -export default ChatView \ No newline at end of file +export default ChatView diff --git a/packages/chat/src/ui/page/ConversationsList.tsx b/packages/chat/src/ui/page/ConversationsList.tsx index b58d1ac..203e60f 100644 --- a/packages/chat/src/ui/page/ConversationsList.tsx +++ b/packages/chat/src/ui/page/ConversationsList.tsx @@ -1,20 +1,27 @@ import * as React from 'react' -import { use } from "react"; +import {use} from 'react' +import {get} from 'radash' +import type {PayloadRequest} from 'payload' +import {queryChats} from '../hooks/queryConversations' interface ConversationsListProps { - // biome-ignore lint/suspicious/noConfusingVoidType: - conversations: Promise|void> + req: PayloadRequest + chatId: undefined | string | number } -export const ConversationsList = ({ - conversations, -}: ConversationsListProps) => { +export const ConversationsList = ({ req, chatId }: ConversationsListProps) => { + const conversations = queryChats(req) const convos = use(conversations) return (
{convos?.map((convo) => ( - +

{convo.description}

diff --git a/packages/chat/src/ui/page/parseParam.ts b/packages/chat/src/ui/page/parseParam.ts new file mode 100644 index 0000000..0b909a2 --- /dev/null +++ b/packages/chat/src/ui/page/parseParam.ts @@ -0,0 +1,14 @@ +import type {AdminViewProps} from 'payload' +import {first, isArray, isString} from 'radash' + +export const getParamValue = ( + params: AdminViewProps['params'], + key: string, +) => { + if (!params) return undefined + if (!params[key]) return undefined + const value = params[key] + if (isString(value)) return value + if (isArray(value)) return first(value) ?? undefined + return undefined +} diff --git a/packages/chat/tsconfig.json b/packages/chat/tsconfig.json index ca5d5eb..bb83e5d 100644 --- a/packages/chat/tsconfig.json +++ b/packages/chat/tsconfig.json @@ -3,7 +3,8 @@ "files": [], "compilerOptions": { "jsx": "react-jsx", - "target": "ES2022" + "target": "ES2022", + "paths": { "@payload-config": ["./src/payload.config.ts"] } }, "references": [ { "path": "./tsconfig.lib.json" }, diff --git a/playground/db/playground.db b/playground/db/playground.db deleted file mode 100644 index 9d42746..0000000 Binary files a/playground/db/playground.db and /dev/null differ diff --git a/playground/src/payload-types.ts b/playground/src/payload-types.ts index 2d887e3..14e9bce 100644 --- a/playground/src/payload-types.ts +++ b/playground/src/payload-types.ts @@ -586,22 +586,15 @@ export interface FormSubmission { */ export interface Chat { id: number; - messages?: - | { - sent: string; - text: string; - role: 'assistant' | 'user'; - context?: { - collection?: string | null; - view?: ('list' | 'record') | null; - }; - id?: string | null; - }[] - | null; - user: { - relationTo: 'users'; - value: number | User; - }; + messages: { + sent?: string | null; + role: 'assistant' | 'user'; + text: string; + collection?: string | null; + view?: ('list' | 'record') | null; + id?: string | null; + }[]; + user: number | User; description?: string | null; updatedAt: string; createdAt: string; diff --git a/playground/src/payload.config.ts b/playground/src/payload.config.ts index 0221d7a..97ef340 100644 --- a/playground/src/payload.config.ts +++ b/playground/src/payload.config.ts @@ -47,6 +47,7 @@ const generateURL: GenerateURL = ({ doc }) => { } export default buildConfig({ + i18n:{fallbackLanguage:'en'}, admin: { components: { // The `BeforeLogin` component renders a message that you see while logging into your admin panel.