feat: add ability to share chats (#594)
This commit is contained in:
parent
64d1aad2bb
commit
4d901ba066
20 changed files with 877 additions and 66 deletions
|
|
@ -7,7 +7,9 @@ import { customModel } from '@/lib/ai';
|
|||
import {
|
||||
deleteMessagesByChatIdAfterTimestamp,
|
||||
getMessageById,
|
||||
updateChatVisiblityById,
|
||||
} from '@/lib/db/queries';
|
||||
import { VisibilityType } from '@/components/visibility-selector';
|
||||
|
||||
export async function saveModelId(model: string) {
|
||||
const cookieStore = await cookies();
|
||||
|
|
@ -40,3 +42,13 @@ export async function deleteTrailingMessages({ id }: { id: string }) {
|
|||
timestamp: message.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateChatVisibility({
|
||||
chatId,
|
||||
visibility,
|
||||
}: {
|
||||
chatId: string;
|
||||
visibility: VisibilityType;
|
||||
}) {
|
||||
await updateChatVisiblityById({ chatId, visibility });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
|
||||
const session = await auth();
|
||||
|
||||
if (!session || !session.user) {
|
||||
return notFound();
|
||||
}
|
||||
if (chat.visibility === 'private') {
|
||||
if (!session || !session.user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (session.user.id !== chat.userId) {
|
||||
return notFound();
|
||||
if (session.user.id !== chat.userId) {
|
||||
return notFound();
|
||||
}
|
||||
}
|
||||
|
||||
const messagesFromDb = await getMessagesByChatId({
|
||||
|
|
@ -41,6 +43,8 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
id={chat.id}
|
||||
initialMessages={convertToUIMessages(messagesFromDb)}
|
||||
selectedModelId={selectedModelId}
|
||||
selectedVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export default async function Page() {
|
|||
id={id}
|
||||
initialMessages={[]}
|
||||
selectedModelId={selectedModelId}
|
||||
selectedVisibilityType="private"
|
||||
isReadonly={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface BlockMessagesProps {
|
|||
reload: (
|
||||
chatRequestOptions?: ChatRequestOptions,
|
||||
) => Promise<string | null | undefined>;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
|
||||
function PureBlockMessages({
|
||||
|
|
@ -29,6 +30,7 @@ function PureBlockMessages({
|
|||
messages,
|
||||
setMessages,
|
||||
reload,
|
||||
isReadonly,
|
||||
}: BlockMessagesProps) {
|
||||
const [messagesContainerRef, messagesEndRef] =
|
||||
useScrollToBottom<HTMLDivElement>();
|
||||
|
|
@ -53,6 +55,7 @@ function PureBlockMessages({
|
|||
}
|
||||
setMessages={setMessages}
|
||||
reload={reload}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ function PureBlock({
|
|||
setMessages,
|
||||
reload,
|
||||
votes,
|
||||
isReadonly,
|
||||
}: {
|
||||
chatId: string;
|
||||
input: string;
|
||||
|
|
@ -86,6 +87,7 @@ function PureBlock({
|
|||
reload: (
|
||||
chatRequestOptions?: ChatRequestOptions,
|
||||
) => Promise<string | null | undefined>;
|
||||
isReadonly: boolean;
|
||||
}) {
|
||||
const {
|
||||
data: documents,
|
||||
|
|
@ -292,6 +294,7 @@ function PureBlock({
|
|||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
reload={reload}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
|
||||
<form className="flex flex-row gap-2 relative items-end w-full px-4 pb-4">
|
||||
|
|
|
|||
|
|
@ -11,8 +11,19 @@ import { PlusIcon, VercelIcon } from './icons';
|
|||
import { useSidebar } from './ui/sidebar';
|
||||
import { memo } from 'react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { VisibilityType, VisibilitySelector } from './visibility-selector';
|
||||
|
||||
function PureChatHeader({ selectedModelId }: { selectedModelId: string }) {
|
||||
function PureChatHeader({
|
||||
chatId,
|
||||
selectedModelId,
|
||||
selectedVisibilityType,
|
||||
isReadonly,
|
||||
}: {
|
||||
chatId: string;
|
||||
selectedModelId: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { open } = useSidebar();
|
||||
|
||||
|
|
@ -21,6 +32,7 @@ function PureChatHeader({ selectedModelId }: { selectedModelId: string }) {
|
|||
return (
|
||||
<header className="flex sticky top-0 bg-background py-1.5 items-center px-2 md:px-2 gap-2">
|
||||
<SidebarToggle />
|
||||
|
||||
{(!open || windowWidth < 768) && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -39,10 +51,22 @@ function PureChatHeader({ selectedModelId }: { selectedModelId: string }) {
|
|||
<TooltipContent>New Chat</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ModelSelector
|
||||
selectedModelId={selectedModelId}
|
||||
className="order-1 md:order-2"
|
||||
/>
|
||||
|
||||
{!isReadonly && (
|
||||
<ModelSelector
|
||||
selectedModelId={selectedModelId}
|
||||
className="order-1 md:order-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isReadonly && (
|
||||
<VisibilitySelector
|
||||
chatId={chatId}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
className="order-1 md:order-3"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="bg-zinc-900 dark:bg-zinc-100 hover:bg-zinc-800 dark:hover:bg-zinc-200 text-zinc-50 dark:text-zinc-900 hidden md:flex py-1.5 px-2 h-fit md:h-[34px] order-4 md:ml-auto"
|
||||
asChild
|
||||
|
|
|
|||
|
|
@ -15,15 +15,20 @@ import { Block, type UIBlock } from './block';
|
|||
import { BlockStreamHandler } from './block-stream-handler';
|
||||
import { MultimodalInput } from './multimodal-input';
|
||||
import { Messages } from './messages';
|
||||
import { VisibilityType } from './visibility-selector';
|
||||
|
||||
export function Chat({
|
||||
id,
|
||||
initialMessages,
|
||||
selectedModelId,
|
||||
selectedVisibilityType,
|
||||
isReadonly,
|
||||
}: {
|
||||
id: string;
|
||||
initialMessages: Array<Message>;
|
||||
selectedModelId: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
}) {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
|
|
@ -74,7 +79,12 @@ export function Chat({
|
|||
return (
|
||||
<>
|
||||
<div className="flex flex-col min-w-0 h-dvh bg-background">
|
||||
<ChatHeader selectedModelId={selectedModelId} />
|
||||
<ChatHeader
|
||||
chatId={id}
|
||||
selectedModelId={selectedModelId}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
|
||||
<Messages
|
||||
chatId={id}
|
||||
|
|
@ -85,22 +95,25 @@ export function Chat({
|
|||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
reload={reload}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
|
||||
<form className="flex mx-auto px-4 bg-background pb-4 md:pb-6 gap-2 w-full md:max-w-3xl">
|
||||
<MultimodalInput
|
||||
chatId={id}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
handleSubmit={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
stop={stop}
|
||||
attachments={attachments}
|
||||
setAttachments={setAttachments}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
append={append}
|
||||
/>
|
||||
{!isReadonly && (
|
||||
<MultimodalInput
|
||||
chatId={id}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
handleSubmit={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
stop={stop}
|
||||
attachments={attachments}
|
||||
setAttachments={setAttachments}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
append={append}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
@ -122,6 +135,7 @@ export function Chat({
|
|||
setMessages={setMessages}
|
||||
reload={reload}
|
||||
votes={votes}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { memo, type SetStateAction } from 'react';
|
|||
|
||||
import type { UIBlock } from './block';
|
||||
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const getActionText = (
|
||||
type: 'create' | 'update' | 'request-suggestions',
|
||||
|
|
@ -26,18 +27,27 @@ interface DocumentToolResultProps {
|
|||
result: { id: string; title: string };
|
||||
block: UIBlock;
|
||||
setBlock: (value: SetStateAction<UIBlock>) => void;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
|
||||
function PureDocumentToolResult({
|
||||
type,
|
||||
result,
|
||||
setBlock,
|
||||
isReadonly,
|
||||
}: DocumentToolResultProps) {
|
||||
return (
|
||||
<button
|
||||
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 = {
|
||||
|
|
@ -79,14 +89,27 @@ interface DocumentToolCallProps {
|
|||
type: 'create' | 'update' | 'request-suggestions';
|
||||
args: { title: string };
|
||||
setBlock: (value: SetStateAction<UIBlock>) => void;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
|
||||
function PureDocumentToolCall({ type, args, setBlock }: DocumentToolCallProps) {
|
||||
function PureDocumentToolCall({
|
||||
type,
|
||||
args,
|
||||
setBlock,
|
||||
isReadonly,
|
||||
}: DocumentToolCallProps) {
|
||||
return (
|
||||
<button
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -855,3 +855,79 @@ export const CheckCirclFillIcon = ({ size = 16 }: { size?: number }) => {
|
|||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GlobeIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LockIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 4.5V6H6V4.5C6 3.39543 6.89543 2.5 8 2.5C9.10457 2.5 10 3.39543 10 4.5ZM4.5 6V4.5C4.5 2.567 6.067 1 8 1C9.933 1 11.5 2.567 11.5 4.5V6H12.5H14V7.5V12.5C14 13.8807 12.8807 15 11.5 15H4.5C3.11929 15 2 13.8807 2 12.5V7.5V6H3.5H4.5ZM11.5 7.5H10H6H4.5H3.5V12.5C3.5 13.0523 3.94772 13.5 4.5 13.5H11.5C12.0523 13.5 12.5 13.0523 12.5 12.5V7.5H11.5Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const EyeIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.02168 4.76932C6.11619 2.33698 9.88374 2.33698 11.9783 4.76932L14.7602 7.99999L11.9783 11.2307C9.88374 13.663 6.1162 13.663 4.02168 11.2307L1.23971 7.99999L4.02168 4.76932ZM13.1149 3.79054C10.422 0.663244 5.57797 0.663247 2.88503 3.79054L-0.318359 7.5106V8.48938L2.88503 12.2094C5.57797 15.3367 10.422 15.3367 13.1149 12.2094L16.3183 8.48938V7.5106L13.1149 3.79054ZM6.49997 7.99999C6.49997 7.17157 7.17154 6.49999 7.99997 6.49999C8.82839 6.49999 9.49997 7.17157 9.49997 7.99999C9.49997 8.82842 8.82839 9.49999 7.99997 9.49999C7.17154 9.49999 6.49997 8.82842 6.49997 7.99999ZM7.99997 4.99999C6.34311 4.99999 4.99997 6.34314 4.99997 7.99999C4.99997 9.65685 6.34311 11 7.99997 11C9.65682 11 11 9.65685 11 7.99999C11 6.34314 9.65682 4.99999 7.99997 4.99999Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShareIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15 11.25V10.5H13.5V11.25V12.75C13.5 13.1642 13.1642 13.5 12.75 13.5H3.25C2.83579 13.5 2.5 13.1642 2.5 12.75L2.5 3.25C2.5 2.83579 2.83579 2.5 3.25 2.5H5.75H6.5V1H5.75H3.25C2.00736 1 1 2.00736 1 3.25V12.75C1 13.9926 2.00736 15 3.25 15H12.75C13.9926 15 15 13.9926 15 12.75V11.25ZM15 5.5L10.5 1V4C7.46243 4 5 6.46243 5 9.5V10L5.05855 9.91218C6.27146 8.09281 8.31339 7 10.5 7V10L15 5.5Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const PurePreviewMessage = ({
|
|||
isLoading,
|
||||
setMessages,
|
||||
reload,
|
||||
isReadonly,
|
||||
}: {
|
||||
chatId: string;
|
||||
message: Message;
|
||||
|
|
@ -42,6 +43,7 @@ const PurePreviewMessage = ({
|
|||
reload: (
|
||||
chatRequestOptions?: ChatRequestOptions,
|
||||
) => Promise<string | null | undefined>;
|
||||
isReadonly: boolean;
|
||||
}) => {
|
||||
const [mode, setMode] = useState<'view' | 'edit'>('view');
|
||||
|
||||
|
|
@ -81,7 +83,7 @@ const PurePreviewMessage = ({
|
|||
|
||||
{message.content && mode === 'view' && (
|
||||
<div className="flex flex-row gap-2 items-start">
|
||||
{message.role === 'user' && (
|
||||
{message.role === 'user' && !isReadonly && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -141,6 +143,7 @@ const PurePreviewMessage = ({
|
|||
result={result}
|
||||
block={block}
|
||||
setBlock={setBlock}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
) : toolName === 'updateDocument' ? (
|
||||
<DocumentToolResult
|
||||
|
|
@ -148,6 +151,7 @@ const PurePreviewMessage = ({
|
|||
result={result}
|
||||
block={block}
|
||||
setBlock={setBlock}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
) : toolName === 'requestSuggestions' ? (
|
||||
<DocumentToolResult
|
||||
|
|
@ -155,6 +159,7 @@ const PurePreviewMessage = ({
|
|||
result={result}
|
||||
block={block}
|
||||
setBlock={setBlock}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
) : (
|
||||
<pre>{JSON.stringify(result, null, 2)}</pre>
|
||||
|
|
@ -176,18 +181,21 @@ const PurePreviewMessage = ({
|
|||
type="create"
|
||||
args={args}
|
||||
setBlock={setBlock}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
) : toolName === 'updateDocument' ? (
|
||||
<DocumentToolCall
|
||||
type="update"
|
||||
args={args}
|
||||
setBlock={setBlock}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
) : toolName === 'requestSuggestions' ? (
|
||||
<DocumentToolCall
|
||||
type="request-suggestions"
|
||||
args={args}
|
||||
setBlock={setBlock}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -196,13 +204,15 @@ const PurePreviewMessage = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<MessageActions
|
||||
key={`action-${message.id}`}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
vote={vote}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{!isReadonly && (
|
||||
<MessageActions
|
||||
key={`action-${message.id}`}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
vote={vote}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ interface MessagesProps {
|
|||
reload: (
|
||||
chatRequestOptions?: ChatRequestOptions,
|
||||
) => Promise<string | null | undefined>;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
|
||||
function PureMessages({
|
||||
|
|
@ -30,6 +31,7 @@ function PureMessages({
|
|||
messages,
|
||||
setMessages,
|
||||
reload,
|
||||
isReadonly,
|
||||
}: MessagesProps) {
|
||||
const [messagesContainerRef, messagesEndRef] =
|
||||
useScrollToBottom<HTMLDivElement>();
|
||||
|
|
@ -56,6 +58,7 @@ function PureMessages({
|
|||
}
|
||||
setMessages={setMessages}
|
||||
reload={reload}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function ModelSelector({
|
|||
const [optimisticModelId, setOptimisticModelId] =
|
||||
useOptimistic(selectedModelId);
|
||||
|
||||
const selectModel = useMemo(
|
||||
const selectedModel = useMemo(
|
||||
() => models.find((model) => model.id === optimisticModelId),
|
||||
[optimisticModelId],
|
||||
);
|
||||
|
|
@ -40,7 +40,7 @@ export function ModelSelector({
|
|||
)}
|
||||
>
|
||||
<Button variant="outline" className="md:px-2 md:h-[34px]">
|
||||
{selectModel?.label}
|
||||
{selectedModel?.label}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@ import { memo, useEffect, useState } from 'react';
|
|||
import { toast } from 'sonner';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { MoreHorizontalIcon, TrashIcon } from '@/components/icons';
|
||||
import {
|
||||
CheckCirclFillIcon,
|
||||
EyeIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
MoreHorizontalIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
} from '@/components/icons';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -23,6 +31,11 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
|
|
@ -36,7 +49,7 @@ import {
|
|||
} from '@/components/ui/sidebar';
|
||||
import type { Chat } from '@/lib/db/schema';
|
||||
import { fetcher } from '@/lib/utils';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { useChatVisibility } from '@/hooks/use-chat-visibility';
|
||||
|
||||
type GroupedChats = {
|
||||
today: Chat[];
|
||||
|
|
@ -56,36 +69,79 @@ const PureChatItem = ({
|
|||
isActive: boolean;
|
||||
onDelete: (chatId: string) => void;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
}) => (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link href={`/chat/${chat.id}`} onClick={() => setOpenMobile(false)}>
|
||||
<span>{chat.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
}) => {
|
||||
const { visibilityType, setVisibilityType } = useChatVisibility({
|
||||
chatId: chat.id,
|
||||
initialVisibility: chat.visibility,
|
||||
});
|
||||
|
||||
<DropdownMenu modal={true}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground mr-0.5"
|
||||
showOnHover={!isActive}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive dark:text-red-500"
|
||||
onSelect={() => onDelete(chat.id)}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link href={`/chat/${chat.id}`} onClick={() => setOpenMobile(false)}>
|
||||
<span>{chat.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<DropdownMenu modal={true}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground mr-0.5"
|
||||
showOnHover={!isActive}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-pointer">
|
||||
<ShareIcon />
|
||||
<span>Share</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer flex-row justify-between"
|
||||
onClick={() => {
|
||||
setVisibilityType('private');
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<LockIcon size={12} />
|
||||
<span>Private</span>
|
||||
</div>
|
||||
{visibilityType === 'private' ? <CheckCirclFillIcon /> : null}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer flex-row justify-between"
|
||||
onClick={() => {
|
||||
setVisibilityType('public');
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<GlobeIcon />
|
||||
<span>Public</span>
|
||||
</div>
|
||||
{visibilityType === 'public' ? <CheckCirclFillIcon /> : null}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive dark:text-red-500"
|
||||
onSelect={() => onDelete(chat.id)}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChatItem = memo(PureChatItem, (prevProps, nextProps) => {
|
||||
if (prevProps.isActive !== nextProps.isActive) return false;
|
||||
|
|
|
|||
109
components/visibility-selector.tsx
Normal file
109
components/visibility-selector.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
'use client';
|
||||
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
CheckCirclFillIcon,
|
||||
ChevronDownIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
} from './icons';
|
||||
import { useChatVisibility } from '@/hooks/use-chat-visibility';
|
||||
|
||||
export type VisibilityType = 'private' | 'public';
|
||||
|
||||
const visibilities: Array<{
|
||||
id: VisibilityType;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
}> = [
|
||||
{
|
||||
id: 'private',
|
||||
label: 'Private',
|
||||
description: 'Only you can access this chat',
|
||||
icon: <LockIcon />,
|
||||
},
|
||||
{
|
||||
id: 'public',
|
||||
label: 'Public',
|
||||
description: 'Anyone with the link can access this chat',
|
||||
icon: <GlobeIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
export function VisibilitySelector({
|
||||
chatId,
|
||||
className,
|
||||
selectedVisibilityType,
|
||||
}: {
|
||||
chatId: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
} & React.ComponentProps<typeof Button>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { visibilityType, setVisibilityType } = useChatVisibility({
|
||||
chatId,
|
||||
initialVisibility: selectedVisibilityType,
|
||||
});
|
||||
|
||||
const selectedVisibility = useMemo(
|
||||
() => visibilities.find((visibility) => visibility.id === visibilityType),
|
||||
[visibilityType],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={cn(
|
||||
'w-fit data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden md:flex md:px-2 md:h-[34px]"
|
||||
>
|
||||
{selectedVisibility?.icon}
|
||||
{selectedVisibility?.label}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start" className="min-w-[300px]">
|
||||
{visibilities.map((visibility) => (
|
||||
<DropdownMenuItem
|
||||
key={visibility.id}
|
||||
onSelect={() => {
|
||||
setVisibilityType(visibility.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="gap-4 group/item flex flex-row justify-between items-center"
|
||||
data-active={visibility.id === visibilityType}
|
||||
>
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
{visibility.label}
|
||||
{visibility.description && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{visibility.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-primary dark:text-primary-foreground opacity-0 group-data-[active=true]/item:opacity-100">
|
||||
<CheckCirclFillIcon />
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
62
hooks/use-chat-visibility.ts
Normal file
62
hooks/use-chat-visibility.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
'use client';
|
||||
|
||||
import { updateChatVisibility } from '@/app/(chat)/actions';
|
||||
import { VisibilityType } from '@/components/visibility-selector';
|
||||
import { Chat } from '@/lib/db/schema';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
|
||||
export function useChatVisibility({
|
||||
chatId,
|
||||
initialVisibility,
|
||||
}: {
|
||||
chatId: string;
|
||||
initialVisibility: VisibilityType;
|
||||
}) {
|
||||
const { mutate, cache } = useSWRConfig();
|
||||
const history: Array<Chat> = cache.get('/api/history')?.data;
|
||||
|
||||
const { data: localVisibility, mutate: setLocalVisibility } = useSWR(
|
||||
`${chatId}-visibility`,
|
||||
null,
|
||||
{
|
||||
fallbackData: initialVisibility,
|
||||
},
|
||||
);
|
||||
|
||||
const visibilityType = useMemo(() => {
|
||||
if (!history) return localVisibility;
|
||||
const chat = history.find((chat) => chat.id === chatId);
|
||||
if (!chat) return 'private';
|
||||
return chat.visibility;
|
||||
}, [history, chatId, localVisibility]);
|
||||
|
||||
const setVisibilityType = (updatedVisibilityType: VisibilityType) => {
|
||||
setLocalVisibility(updatedVisibilityType);
|
||||
|
||||
mutate<Array<Chat>>(
|
||||
'/api/history',
|
||||
(history) => {
|
||||
return history
|
||||
? history.map((chat) => {
|
||||
if (chat.id === chatId) {
|
||||
return {
|
||||
...chat,
|
||||
visibility: updatedVisibilityType,
|
||||
};
|
||||
}
|
||||
return chat;
|
||||
})
|
||||
: [];
|
||||
},
|
||||
{ revalidate: false },
|
||||
);
|
||||
|
||||
updateChatVisibility({
|
||||
chatId: chatId,
|
||||
visibility: updatedVisibilityType,
|
||||
});
|
||||
};
|
||||
|
||||
return { visibilityType, setVisibilityType };
|
||||
}
|
||||
1
lib/db/migrations/0003_cloudy_glorian.sql
Normal file
1
lib/db/migrations/0003_cloudy_glorian.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Chat" ADD COLUMN "visibility" varchar DEFAULT 'private' NOT NULL;
|
||||
384
lib/db/migrations/meta/0003_snapshot.json
Normal file
384
lib/db/migrations/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
{
|
||||
"id": "011efa9e-42c7-4ff6-830a-02106f6638c9",
|
||||
"prevId": "b5d8e862-936f-4419-a50f-97be3e7fe665",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.Chat": {
|
||||
"name": "Chat",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"visibility": {
|
||||
"name": "visibility",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'private'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Chat_userId_User_id_fk": {
|
||||
"name": "Chat_userId_User_id_fk",
|
||||
"tableFrom": "Chat",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Document": {
|
||||
"name": "Document",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Document_userId_User_id_fk": {
|
||||
"name": "Document_userId_User_id_fk",
|
||||
"tableFrom": "Document",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Document_id_createdAt_pk": {
|
||||
"name": "Document_id_createdAt_pk",
|
||||
"columns": [
|
||||
"id",
|
||||
"createdAt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Message": {
|
||||
"name": "Message",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Message_chatId_Chat_id_fk": {
|
||||
"name": "Message_chatId_Chat_id_fk",
|
||||
"tableFrom": "Message",
|
||||
"tableTo": "Chat",
|
||||
"columnsFrom": [
|
||||
"chatId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Suggestion": {
|
||||
"name": "Suggestion",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"documentId": {
|
||||
"name": "documentId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"documentCreatedAt": {
|
||||
"name": "documentCreatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"originalText": {
|
||||
"name": "originalText",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"suggestedText": {
|
||||
"name": "suggestedText",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"isResolved": {
|
||||
"name": "isResolved",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Suggestion_userId_User_id_fk": {
|
||||
"name": "Suggestion_userId_User_id_fk",
|
||||
"tableFrom": "Suggestion",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
|
||||
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
|
||||
"tableFrom": "Suggestion",
|
||||
"tableTo": "Document",
|
||||
"columnsFrom": [
|
||||
"documentId",
|
||||
"documentCreatedAt"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id",
|
||||
"createdAt"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Suggestion_id_pk": {
|
||||
"name": "Suggestion_id_pk",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.User": {
|
||||
"name": "User",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Vote": {
|
||||
"name": "Vote",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"messageId": {
|
||||
"name": "messageId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"isUpvoted": {
|
||||
"name": "isUpvoted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Vote_chatId_Chat_id_fk": {
|
||||
"name": "Vote_chatId_Chat_id_fk",
|
||||
"tableFrom": "Vote",
|
||||
"tableTo": "Chat",
|
||||
"columnsFrom": [
|
||||
"chatId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"Vote_messageId_Message_id_fk": {
|
||||
"name": "Vote_messageId_Message_id_fk",
|
||||
"tableFrom": "Vote",
|
||||
"tableTo": "Message",
|
||||
"columnsFrom": [
|
||||
"messageId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Vote_chatId_messageId_pk": {
|
||||
"name": "Vote_chatId_messageId_pk",
|
||||
"columns": [
|
||||
"chatId",
|
||||
"messageId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1730725226313,
|
||||
"tag": "0002_wandering_riptide",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1733403031014,
|
||||
"tag": "0003_cloudy_glorian",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -309,3 +309,18 @@ export async function deleteMessagesByChatIdAfterTimestamp({
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateChatVisiblityById({
|
||||
chatId,
|
||||
visibility,
|
||||
}: {
|
||||
chatId: string;
|
||||
visibility: 'private' | 'public';
|
||||
}) {
|
||||
try {
|
||||
return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId));
|
||||
} catch (error) {
|
||||
console.error('Failed to update chat visibility in database');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export const chat = pgTable('Chat', {
|
|||
userId: uuid('userId')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
visibility: varchar('visibility', { enum: ['public', 'private'] })
|
||||
.notNull()
|
||||
.default('private'),
|
||||
});
|
||||
|
||||
export type Chat = InferSelectModel<typeof chat>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue