diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index cf1ecdd89..01b000bf7 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,5 +1,7 @@ import type { NextAuthConfig } from 'next-auth'; +const publicRoutes = ['/api/document']; + export const authConfig = { pages: { signIn: '/login', @@ -15,18 +17,20 @@ export const authConfig = { const isOnChat = nextUrl.pathname.startsWith('/'); const isOnRegister = nextUrl.pathname.startsWith('/register'); const isOnLogin = nextUrl.pathname.startsWith('/login'); + const isPublicRoute = publicRoutes.some((route) => + nextUrl.pathname.startsWith(route), + ); if (isLoggedIn && (isOnLogin || isOnRegister)) { return Response.redirect(new URL('/', nextUrl as unknown as URL)); } - if (isOnRegister || isOnLogin) { - return true; // Always allow access to register and login pages + if (isOnRegister || isOnLogin || isPublicRoute) { + return true; // Always allow access to register and login pages or public routes } if (isOnChat) { - if (isLoggedIn) return true; - return false; // Redirect unauthenticated users to login page + return isLoggedIn; // Redirect unauthenticated users to login page } if (isLoggedIn) { diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index 862d49fdf..2f9e2ee99 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -3,6 +3,7 @@ import type { ArtifactKind } from '@/components/artifact'; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, + getChatById, saveDocument, } from '@/lib/db/queries'; @@ -14,25 +15,30 @@ export async function GET(request: Request) { return new Response('Missing id', { status: 400 }); } - const session = await auth(); + const documents = await getDocumentsById({ id }); + const [document] = documents; - if (!session || !session.user) { - return new Response('Unauthorized', { status: 401 }); + if (!document) { + return new Response('Not Found', { status: 404 }); } - const documents = await getDocumentsById({ id }); + const session = await auth(); - const [document] = documents; + if (session?.user && document.userId === session.user.id) { + return Response.json(documents, { status: 200 }); + } - if (!document) { + const chat = await getChatById({ id: document.chatId }); + + if (!chat) { return new Response('Not Found', { status: 404 }); } - if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + if (chat.visibility === 'public') { + return Response.json(documents, { status: 200 }); } - return Response.json(documents, { status: 200 }); + return new Response('Unauthorized', { status: 401 }); } export async function POST(request: Request) { @@ -53,8 +59,13 @@ export async function POST(request: Request) { content, title, kind, - }: { content: string; title: string; kind: ArtifactKind } = - await request.json(); + chatId, + }: { + content: string; + title: string; + kind: ArtifactKind; + chatId: string; + } = await request.json(); const documents = await getDocumentsById({ id: id }); @@ -72,6 +83,7 @@ export async function POST(request: Request) { title, kind, userId: session.user.id, + chatId, }); return Response.json(document, { status: 200 }); @@ -97,7 +109,6 @@ export async function DELETE(request: Request) { } const documents = await getDocumentsById({ id }); - const [document] = documents; if (document.userId !== session.user.id) { diff --git a/components/artifact.tsx b/components/artifact.tsx index fd3c281cd..9567078d8 100644 --- a/components/artifact.tsx +++ b/components/artifact.tsx @@ -39,6 +39,7 @@ export type ArtifactKind = (typeof artifactDefinitions)[number]['kind']; export interface UIArtifact { title: string; documentId: string; + chatId: string; kind: ArtifactKind; content: string; isVisible: boolean; @@ -146,6 +147,7 @@ function PureArtifact({ title: artifact.title, content: updatedContent, kind: artifact.kind, + chatId: artifact.chatId, }), }); @@ -194,7 +196,7 @@ function PureArtifact({ } const handleVersionChange = (type: 'next' | 'prev' | 'toggle' | 'latest') => { - if (!documents) return; + if (!documents || isReadonly) return; if (type === 'latest') { setCurrentVersionIndex(documents.length - 1); @@ -503,6 +505,7 @@ export const Artifact = memo(PureArtifact, (prevProps, nextProps) => { if (!equal(prevProps.votes, nextProps.votes)) return false; if (prevProps.input !== nextProps.input) return false; if (!equal(prevProps.messages, nextProps.messages.length)) return false; + if (prevProps.isReadonly !== nextProps.isReadonly) return false; return true; }); diff --git a/components/code-editor.tsx b/components/code-editor.tsx index ab81e1390..02246f5e8 100644 --- a/components/code-editor.tsx +++ b/components/code-editor.tsx @@ -6,7 +6,7 @@ import { python } from '@codemirror/lang-python'; import { oneDark } from '@codemirror/theme-one-dark'; import { basicSetup } from 'codemirror'; import React, { memo, useEffect, useRef } from 'react'; -import { Suggestion } from '@/lib/db/schema'; +import type { Suggestion } from '@/lib/db/schema'; type EditorProps = { content: string; @@ -15,9 +15,10 @@ type EditorProps = { isCurrentVersion: boolean; currentVersionIndex: number; suggestions: Array; + isReadonly?: boolean; }; -function PureCodeEditor({ content, onSaveContent, status }: EditorProps) { +function PureCodeEditor({ content, saveContent, status, isReadonly }: EditorProps) { const containerRef = useRef(null); const editorRef = useRef(null); @@ -25,7 +26,13 @@ function PureCodeEditor({ content, onSaveContent, status }: EditorProps) { if (containerRef.current && !editorRef.current) { const startState = EditorState.create({ doc: content, - extensions: [basicSetup, python(), oneDark], + extensions: [ + basicSetup, + python(), + oneDark, + EditorView.editable.of(!isReadonly), + EditorState.readOnly.of(!!isReadonly) + ], }); editorRef.current = new EditorView({ @@ -47,7 +54,7 @@ function PureCodeEditor({ content, onSaveContent, status }: EditorProps) { useEffect(() => { if (editorRef.current) { const updateListener = EditorView.updateListener.of((update) => { - if (update.docChanged) { + if (update.docChanged && !isReadonly) { const transaction = update.transactions.find( (tr) => !tr.annotation(Transaction.remote), ); @@ -63,13 +70,19 @@ function PureCodeEditor({ content, onSaveContent, status }: EditorProps) { const newState = EditorState.create({ doc: editorRef.current.state.doc, - extensions: [basicSetup, python(), oneDark, updateListener], - selection: currentSelection, + extensions: [ + basicSetup, + python(), + oneDark, + updateListener, + EditorView.editable.of(!isReadonly), + EditorState.readOnly.of(!!isReadonly) + ], }); editorRef.current.setState(newState); } - }, [onSaveContent]); + }, [saveContent, isReadonly]); useEffect(() => { if (editorRef.current && content) { @@ -106,8 +119,9 @@ function areEqual(prevProps: EditorProps, nextProps: EditorProps) { if (prevProps.status === 'streaming' && nextProps.status === 'streaming') return false; if (prevProps.content !== nextProps.content) return false; + if (prevProps.isReadonly !== nextProps.isReadonly) return false; return true; } -export const CodeEditor = memo(PureCodeEditor, areEqual); +export const CodeEditor = memo(PureCodeEditor, areEqual); \ No newline at end of file diff --git a/components/document-preview.tsx b/components/document-preview.tsx index 6f2819038..14c07a8ad 100644 --- a/components/document-preview.tsx +++ b/components/document-preview.tsx @@ -2,7 +2,7 @@ import { memo, - MouseEvent, + type MouseEvent, useCallback, useEffect, useMemo, @@ -11,7 +11,7 @@ import { import { ArtifactKind, UIArtifact } from './artifact'; import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from './icons'; import { cn, fetcher } from '@/lib/utils'; -import { Document } from '@/lib/db/schema'; +import type { Document } from '@/lib/db/schema'; import { InlineDocumentSkeleton } from './document-skeleton'; import useSWR from 'swr'; import { Editor } from './text-editor'; @@ -23,15 +23,15 @@ import { SpreadsheetEditor } from './sheet-editor'; import { ImageEditor } from './image-editor'; interface DocumentPreviewProps { - isReadonly: boolean; result?: any; args?: any; + chatId: string; } export function DocumentPreview({ - isReadonly, result, args, + chatId, }: DocumentPreviewProps) { const { artifact, setArtifact } = useArtifact(); @@ -64,7 +64,7 @@ export function DocumentPreview({ ); } @@ -74,7 +74,7 @@ export function DocumentPreview({ ); } @@ -94,6 +94,7 @@ export function DocumentPreview({ id: artifact.documentId, createdAt: new Date(), userId: 'noop', + chatId: artifact.chatId, } : null; @@ -104,6 +105,7 @@ export function DocumentPreview({ ; result: any; setArtifact: ( updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact), ) => void; + chatId: string; }) => { const handleClick = useCallback( (event: MouseEvent) => { @@ -165,6 +169,7 @@ const PureHitboxLayer = ({ documentId: result.id, kind: result.kind, isVisible: true, + chatId, boundingBox: { left: boundingBox.x, top: boundingBox.y, @@ -174,7 +179,7 @@ const PureHitboxLayer = ({ }, ); }, - [setArtifact, result], + [setArtifact, result, chatId], ); return ( diff --git a/components/document.tsx b/components/document.tsx index 9ebc1dea7..9dbccb839 100644 --- a/components/document.tsx +++ b/components/document.tsx @@ -27,12 +27,13 @@ interface DocumentToolResultProps { type: 'create' | 'update' | 'request-suggestions'; result: { id: string; title: string; kind: ArtifactKind }; isReadonly: boolean; + chatId: string; } function PureDocumentToolResult({ type, result, - isReadonly, + chatId, }: DocumentToolResultProps) { const { setArtifact } = useArtifact(); @@ -41,13 +42,6 @@ function PureDocumentToolResult({ type="button" className="bg-background cursor-pointer border py-2 px-3 rounded-xl w-fit flex flex-row gap-3 items-start" onClick={(event) => { - if (isReadonly) { - toast.error( - 'Viewing files in shared chats is currently not supported.', - ); - return; - } - const rect = event.currentTarget.getBoundingClientRect(); const boundingBox = { @@ -65,6 +59,7 @@ function PureDocumentToolResult({ isVisible: true, status: 'idle', boundingBox, + chatId, }); }} > @@ -89,13 +84,13 @@ export const DocumentToolResult = memo(PureDocumentToolResult, () => true); interface DocumentToolCallProps { type: 'create' | 'update' | 'request-suggestions'; args: { title: string }; - isReadonly: boolean; + chatId: string; } function PureDocumentToolCall({ type, args, - isReadonly, + chatId, }: DocumentToolCallProps) { const { setArtifact } = useArtifact(); @@ -104,13 +99,6 @@ function PureDocumentToolCall({ type="button" className="cursor pointer w-fit border py-2 px-3 rounded-xl flex flex-row items-start justify-between gap-3" onClick={(event) => { - if (isReadonly) { - toast.error( - 'Viewing files in shared chats is currently not supported.', - ); - return; - } - const rect = event.currentTarget.getBoundingClientRect(); const boundingBox = { @@ -124,6 +112,7 @@ function PureDocumentToolCall({ ...currentArtifact, isVisible: true, boundingBox, + chatId, })); }} > diff --git a/components/message.tsx b/components/message.tsx index 6b49e8918..fbd057e54 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -163,18 +163,18 @@ const PurePreviewMessage = ({ {toolName === 'getWeather' ? ( ) : toolName === 'createDocument' ? ( - + ) : toolName === 'updateDocument' ? ( ) : toolName === 'requestSuggestions' ? ( ) : null} diff --git a/components/text-editor.tsx b/components/text-editor.tsx index c24dd33e6..fa2e6611e 100644 --- a/components/text-editor.tsx +++ b/components/text-editor.tsx @@ -30,6 +30,7 @@ type EditorProps = { isCurrentVersion: boolean; currentVersionIndex: number; suggestions: Array; + isReadonly?: boolean; }; function PureEditor({ @@ -37,6 +38,7 @@ function PureEditor({ onSaveContent, suggestions, status, + isReadonly, }: EditorProps) { const containerRef = useRef(null); const editorRef = useRef(null); @@ -63,6 +65,7 @@ function PureEditor({ editorRef.current = new EditorView(containerRef.current, { state, + editable: () => !isReadonly, }); } @@ -79,6 +82,7 @@ function PureEditor({ useEffect(() => { if (editorRef.current) { editorRef.current.setProps({ + editable: () => !isReadonly, dispatchTransaction: (transaction) => { handleTransaction({ transaction, @@ -88,7 +92,7 @@ function PureEditor({ }, }); } - }, [onSaveContent]); + }, [saveContent, isReadonly]); useEffect(() => { if (editorRef.current && content) { @@ -157,7 +161,8 @@ function areEqual(prevProps: EditorProps, nextProps: EditorProps) { prevProps.isCurrentVersion === nextProps.isCurrentVersion && !(prevProps.status === 'streaming' && nextProps.status === 'streaming') && prevProps.content === nextProps.content && - prevProps.onSaveContent === nextProps.onSaveContent + prevProps.saveContent === nextProps.saveContent && + prevProps.isReadonly === nextProps.isReadonly ); } diff --git a/hooks/use-artifact.ts b/hooks/use-artifact.ts index 9133fe802..73837174a 100644 --- a/hooks/use-artifact.ts +++ b/hooks/use-artifact.ts @@ -6,6 +6,7 @@ import { useCallback, useMemo } from 'react'; export const initialArtifactData: UIArtifact = { documentId: 'init', + chatId: '', content: '', kind: 'text', title: '', diff --git a/lib/db/queries.ts b/lib/db/queries.ts index d51c5ae20..284a97281 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -17,7 +17,7 @@ import { type DBMessage, Chat, } from './schema'; -import { ArtifactKind } from '@/components/artifact'; +import type { ArtifactKind } from '@/components/artifact'; // Optionally, if not using email/pass login, you can // use the Drizzle adapter for Auth.js / NextAuth @@ -233,12 +233,14 @@ export async function saveDocument({ kind, content, userId, + chatId, }: { id: string; title: string; kind: ArtifactKind; content: string; userId: string; + chatId: string; }) { try { return await db.insert(document).values({ @@ -247,6 +249,7 @@ export async function saveDocument({ kind, content, userId, + chatId, createdAt: new Date(), }); } catch (error) { diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 1228aab9a..dd4bd7a2d 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -115,6 +115,9 @@ export const document = pgTable( userId: uuid('userId') .notNull() .references(() => user.id), + chatId: uuid('chatId') + .notNull() + .references(() => chat.id), }, (table) => { return {