fix: support setting visibility on initial chat creation (#975)
This commit is contained in:
parent
a3221fbcdc
commit
575c12503c
15 changed files with 158 additions and 32 deletions
|
|
@ -49,7 +49,8 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
try {
|
||||
const { id, message, selectedChatModel } = requestBody;
|
||||
const { id, message, selectedChatModel, selectedVisibilityType } =
|
||||
requestBody;
|
||||
|
||||
const session = await auth();
|
||||
|
||||
|
|
@ -80,7 +81,12 @@ export async function POST(request: Request) {
|
|||
message,
|
||||
});
|
||||
|
||||
await saveChat({ id, userId: session.user.id, title });
|
||||
await saveChat({
|
||||
id,
|
||||
userId: session.user.id,
|
||||
title,
|
||||
visibility: selectedVisibilityType,
|
||||
});
|
||||
} else {
|
||||
if (chat.userId !== session.user.id) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
|
|
@ -236,7 +242,7 @@ export async function GET(request: Request) {
|
|||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (chat.userId !== session.user.id) {
|
||||
if (chat.visibility === 'private' && chat.userId !== session.user.id) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const postRequestBodySchema = z.object({
|
|||
.optional(),
|
||||
}),
|
||||
selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']),
|
||||
selectedVisibilityType: z.enum(['public', 'private']),
|
||||
});
|
||||
|
||||
export type PostRequestBody = z.infer<typeof postRequestBodySchema>;
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
<Chat
|
||||
id={chat.id}
|
||||
initialMessages={convertToUIMessages(messagesFromDb)}
|
||||
selectedChatModel={DEFAULT_CHAT_MODEL}
|
||||
selectedVisibilityType={chat.visibility}
|
||||
initialChatModel={DEFAULT_CHAT_MODEL}
|
||||
initialVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
session={session}
|
||||
autoResume={true}
|
||||
|
|
@ -76,8 +76,8 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
<Chat
|
||||
id={chat.id}
|
||||
initialMessages={convertToUIMessages(messagesFromDb)}
|
||||
selectedChatModel={chatModelFromCookie.value}
|
||||
selectedVisibilityType={chat.visibility}
|
||||
initialChatModel={chatModelFromCookie.value}
|
||||
initialVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
session={session}
|
||||
autoResume={true}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ export default async function Page() {
|
|||
key={id}
|
||||
id={id}
|
||||
initialMessages={[]}
|
||||
selectedChatModel={DEFAULT_CHAT_MODEL}
|
||||
selectedVisibilityType="private"
|
||||
initialChatModel={DEFAULT_CHAT_MODEL}
|
||||
initialVisibilityType="private"
|
||||
isReadonly={false}
|
||||
session={session}
|
||||
autoResume={false}
|
||||
|
|
@ -43,8 +43,8 @@ export default async function Page() {
|
|||
key={id}
|
||||
id={id}
|
||||
initialMessages={[]}
|
||||
selectedChatModel={modelIdFromCookie.value}
|
||||
selectedVisibilityType="private"
|
||||
initialChatModel={modelIdFromCookie.value}
|
||||
initialVisibilityType="private"
|
||||
isReadonly={false}
|
||||
session={session}
|
||||
autoResume={false}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { sheetArtifact } from '@/artifacts/sheet/client';
|
|||
import { textArtifact } from '@/artifacts/text/client';
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
|
||||
export const artifactDefinitions = [
|
||||
textArtifact,
|
||||
|
|
@ -66,6 +67,7 @@ function PureArtifact({
|
|||
reload,
|
||||
votes,
|
||||
isReadonly,
|
||||
selectedVisibilityType,
|
||||
}: {
|
||||
chatId: string;
|
||||
input: string;
|
||||
|
|
@ -81,6 +83,7 @@ function PureArtifact({
|
|||
handleSubmit: UseChatHelpers['handleSubmit'];
|
||||
reload: UseChatHelpers['reload'];
|
||||
isReadonly: boolean;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
}) {
|
||||
const { artifact, setArtifact, metadata, setMetadata } = useArtifact();
|
||||
|
||||
|
|
@ -335,6 +338,7 @@ function PureArtifact({
|
|||
append={append}
|
||||
className="bg-background dark:bg-muted"
|
||||
setMessages={setMessages}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -503,6 +507,8 @@ 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.selectedVisibilityType !== nextProps.selectedVisibilityType)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,26 +17,32 @@ import { getChatHistoryPaginationKey } from './sidebar-history';
|
|||
import { toast } from './toast';
|
||||
import type { Session } from 'next-auth';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useChatVisibility } from '@/hooks/use-chat-visibility';
|
||||
|
||||
export function Chat({
|
||||
id,
|
||||
initialMessages,
|
||||
selectedChatModel,
|
||||
selectedVisibilityType,
|
||||
initialChatModel,
|
||||
initialVisibilityType,
|
||||
isReadonly,
|
||||
session,
|
||||
autoResume,
|
||||
}: {
|
||||
id: string;
|
||||
initialMessages: Array<UIMessage>;
|
||||
selectedChatModel: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
initialChatModel: string;
|
||||
initialVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
session: Session;
|
||||
autoResume: boolean;
|
||||
}) {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { visibilityType } = useChatVisibility({
|
||||
chatId: id,
|
||||
initialVisibilityType,
|
||||
});
|
||||
|
||||
const {
|
||||
messages,
|
||||
setMessages,
|
||||
|
|
@ -57,7 +63,8 @@ export function Chat({
|
|||
experimental_prepareRequestBody: (body) => ({
|
||||
id,
|
||||
message: body.messages.at(-1),
|
||||
selectedChatModel,
|
||||
selectedChatModel: initialChatModel,
|
||||
selectedVisibilityType: visibilityType,
|
||||
}),
|
||||
onFinish: () => {
|
||||
mutate(unstable_serialize(getChatHistoryPaginationKey));
|
||||
|
|
@ -109,8 +116,8 @@ export function Chat({
|
|||
<div className="flex flex-col min-w-0 h-dvh bg-background">
|
||||
<ChatHeader
|
||||
chatId={id}
|
||||
selectedModelId={selectedChatModel}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
selectedModelId={initialChatModel}
|
||||
selectedVisibilityType={initialVisibilityType}
|
||||
isReadonly={isReadonly}
|
||||
session={session}
|
||||
/>
|
||||
|
|
@ -140,6 +147,7 @@ export function Chat({
|
|||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
append={append}
|
||||
selectedVisibilityType={visibilityType}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
|
|
@ -160,6 +168,7 @@ export function Chat({
|
|||
reload={reload}
|
||||
votes={votes}
|
||||
isReadonly={isReadonly}
|
||||
selectedVisibilityType={visibilityType}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import type { UseChatHelpers } from '@ai-sdk/react';
|
|||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ArrowDown } from 'lucide-react';
|
||||
import { useScrollToBottom } from '@/hooks/use-scroll-to-bottom';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
|
||||
function PureMultimodalInput({
|
||||
chatId,
|
||||
|
|
@ -40,6 +41,7 @@ function PureMultimodalInput({
|
|||
append,
|
||||
handleSubmit,
|
||||
className,
|
||||
selectedVisibilityType,
|
||||
}: {
|
||||
chatId: string;
|
||||
input: UseChatHelpers['input'];
|
||||
|
|
@ -53,6 +55,7 @@ function PureMultimodalInput({
|
|||
append: UseChatHelpers['append'];
|
||||
handleSubmit: UseChatHelpers['handleSubmit'];
|
||||
className?: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
}) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { width } = useWindowSize();
|
||||
|
|
@ -220,7 +223,11 @@ function PureMultimodalInput({
|
|||
{messages.length === 0 &&
|
||||
attachments.length === 0 &&
|
||||
uploadQueue.length === 0 && (
|
||||
<SuggestedActions append={append} chatId={chatId} />
|
||||
<SuggestedActions
|
||||
append={append}
|
||||
chatId={chatId}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
|
|
@ -309,6 +316,8 @@ export const MultimodalInput = memo(
|
|||
if (prevProps.input !== nextProps.input) return false;
|
||||
if (prevProps.status !== nextProps.status) return false;
|
||||
if (!equal(prevProps.attachments, nextProps.attachments)) return false;
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Chat } from '@/lib/db/schema';
|
||||
import type { Chat } from '@/lib/db/schema';
|
||||
import {
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
|
|
@ -39,7 +39,7 @@ const PureChatItem = ({
|
|||
}) => {
|
||||
const { visibilityType, setVisibilityType } = useChatVisibility({
|
||||
chatId: chat.id,
|
||||
initialVisibility: chat.visibility,
|
||||
initialVisibilityType: chat.visibility,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,14 +3,20 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { Button } from './ui/button';
|
||||
import { memo } from 'react';
|
||||
import { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
|
||||
interface SuggestedActionsProps {
|
||||
chatId: string;
|
||||
append: UseChatHelpers['append'];
|
||||
selectedVisibilityType: VisibilityType;
|
||||
}
|
||||
|
||||
function PureSuggestedActions({ chatId, append }: SuggestedActionsProps) {
|
||||
function PureSuggestedActions({
|
||||
chatId,
|
||||
append,
|
||||
selectedVisibilityType,
|
||||
}: SuggestedActionsProps) {
|
||||
const suggestedActions = [
|
||||
{
|
||||
title: 'What are the advantages',
|
||||
|
|
@ -71,4 +77,13 @@ function PureSuggestedActions({ chatId, append }: SuggestedActionsProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export const SuggestedActions = memo(PureSuggestedActions, () => true);
|
||||
export const SuggestedActions = memo(
|
||||
PureSuggestedActions,
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.chatId !== nextProps.chatId) return false;
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { type ReactNode, useMemo, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -9,7 +9,6 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
CheckCircleFillIcon,
|
||||
ChevronDownIcon,
|
||||
|
|
@ -52,7 +51,7 @@ export function VisibilitySelector({
|
|||
|
||||
const { visibilityType, setVisibilityType } = useChatVisibility({
|
||||
chatId,
|
||||
initialVisibility: selectedVisibilityType,
|
||||
initialVisibilityType: selectedVisibilityType,
|
||||
});
|
||||
|
||||
const selectedVisibility = useMemo(
|
||||
|
|
@ -70,6 +69,7 @@ export function VisibilitySelector({
|
|||
)}
|
||||
>
|
||||
<Button
|
||||
data-testid="visibility-selector"
|
||||
variant="outline"
|
||||
className="hidden md:flex md:px-2 md:h-[34px]"
|
||||
>
|
||||
|
|
@ -82,6 +82,7 @@ export function VisibilitySelector({
|
|||
<DropdownMenuContent align="start" className="min-w-[300px]">
|
||||
{visibilities.map((visibility) => (
|
||||
<DropdownMenuItem
|
||||
data-testid={`visibility-selector-item-${visibility.id}`}
|
||||
key={visibility.id}
|
||||
onSelect={() => {
|
||||
setVisibilityType(visibility.id);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import type { VisibilityType } from '@/components/visibility-selector';
|
|||
|
||||
export function useChatVisibility({
|
||||
chatId,
|
||||
initialVisibility,
|
||||
initialVisibilityType,
|
||||
}: {
|
||||
chatId: string;
|
||||
initialVisibility: VisibilityType;
|
||||
initialVisibilityType: VisibilityType;
|
||||
}) {
|
||||
const { mutate, cache } = useSWRConfig();
|
||||
const history: ChatHistory = cache.get('/api/history')?.data;
|
||||
|
|
@ -24,7 +24,7 @@ export function useChatVisibility({
|
|||
`${chatId}-visibility`,
|
||||
null,
|
||||
{
|
||||
fallbackData: initialVisibility,
|
||||
fallbackData: initialVisibilityType,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
import type { ArtifactKind } from '@/components/artifact';
|
||||
import { generateUUID } from '../utils';
|
||||
import { generateHashedPassword } from './utils';
|
||||
import type { VisibilityType } from '@/components/visibility-selector';
|
||||
|
||||
// Optionally, if not using email/pass login, you can
|
||||
// use the Drizzle adapter for Auth.js / NextAuth
|
||||
|
|
@ -79,10 +80,12 @@ export async function saveChat({
|
|||
id,
|
||||
userId,
|
||||
title,
|
||||
visibility,
|
||||
}: {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
visibility: VisibilityType;
|
||||
}) {
|
||||
try {
|
||||
return await db.insert(chat).values({
|
||||
|
|
@ -90,6 +93,7 @@ export async function saveChat({
|
|||
createdAt: new Date(),
|
||||
userId,
|
||||
title,
|
||||
visibility,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save chat in database');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ai-chatbot",
|
||||
"version": "3.0.16",
|
||||
"version": "3.0.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
|
|
|
|||
|
|
@ -115,6 +115,23 @@ export class ChatPage {
|
|||
expect(await this.getSelectedModel()).toBe(chatModel.name);
|
||||
}
|
||||
|
||||
public async getSelectedVisibility() {
|
||||
const visibilityId = await this.page
|
||||
.getByTestId('visibility-selector')
|
||||
.innerText();
|
||||
return visibilityId;
|
||||
}
|
||||
|
||||
public async chooseVisibilityFromSelector(
|
||||
chatVisibility: 'public' | 'private',
|
||||
) {
|
||||
await this.page.getByTestId('visibility-selector').click();
|
||||
await this.page
|
||||
.getByTestId(`visibility-selector-item-${chatVisibility}`)
|
||||
.click();
|
||||
expect(await this.getSelectedVisibility()).toBe(chatVisibility);
|
||||
}
|
||||
|
||||
async getRecentAssistantMessage() {
|
||||
const messageElements = await this.page
|
||||
.getByTestId('message-assistant')
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ test.describe
|
|||
id: chatId,
|
||||
message: TEST_PROMPTS.SKY.MESSAGE,
|
||||
selectedChatModel: 'chat-model',
|
||||
selectedVisibilityType: 'private',
|
||||
},
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
|
|
@ -49,6 +50,7 @@ test.describe
|
|||
id: chatId,
|
||||
message: TEST_PROMPTS.GRASS.MESSAGE,
|
||||
selectedChatModel: 'chat-model',
|
||||
selectedVisibilityType: 'private',
|
||||
},
|
||||
});
|
||||
expect(response.status()).toBe(403);
|
||||
|
|
@ -109,6 +111,7 @@ test.describe
|
|||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
selectedChatModel: 'chat-model',
|
||||
selectedVisibilityType: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -162,6 +165,7 @@ test.describe
|
|||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
selectedChatModel: 'chat-model',
|
||||
selectedVisibilityType: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -190,7 +194,7 @@ test.describe
|
|||
expect(secondResponseContent).toEqual('');
|
||||
});
|
||||
|
||||
test('Babbage cannot resume chat generation that belongs to Ada', async ({
|
||||
test('Babbage cannot resume a private chat generation that belongs to Ada', async ({
|
||||
adaContext,
|
||||
babbageContext,
|
||||
}) => {
|
||||
|
|
@ -212,6 +216,7 @@ test.describe
|
|||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
selectedChatModel: 'chat-model',
|
||||
selectedVisibilityType: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -234,4 +239,57 @@ test.describe
|
|||
expect(firstStatusCode).toBe(200);
|
||||
expect(secondStatusCode).toBe(403);
|
||||
});
|
||||
|
||||
test('Babbage can resume a public chat generation that belongs to Ada', async ({
|
||||
adaContext,
|
||||
babbageContext,
|
||||
}) => {
|
||||
const chatId = generateUUID();
|
||||
|
||||
const firstRequest = adaContext.request.post('/api/chat', {
|
||||
data: {
|
||||
id: chatId,
|
||||
message: {
|
||||
id: generateUUID(),
|
||||
role: 'user',
|
||||
content: 'Help me write an essay about Silicon Valley',
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Help me write an essay about Silicon Valley',
|
||||
},
|
||||
],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
selectedChatModel: 'chat-model',
|
||||
selectedVisibilityType: 'public',
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const secondRequest = babbageContext.request.get(
|
||||
`/api/chat?chatId=${chatId}`,
|
||||
);
|
||||
|
||||
const [firstResponse, secondResponse] = await Promise.all([
|
||||
firstRequest,
|
||||
secondRequest,
|
||||
]);
|
||||
|
||||
const [firstStatusCode, secondStatusCode] = await Promise.all([
|
||||
firstResponse.status(),
|
||||
secondResponse.status(),
|
||||
]);
|
||||
|
||||
expect(firstStatusCode).toBe(200);
|
||||
expect(secondStatusCode).toBe(200);
|
||||
|
||||
const [firstResponseContent, secondResponseContent] = await Promise.all([
|
||||
firstResponse.text(),
|
||||
secondResponse.text(),
|
||||
]);
|
||||
|
||||
expect(firstResponseContent).toEqual(secondResponseContent);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue