Refactor chat permissions and enhance edit capabilities

- Introduced `useChatPermission` hook to manage chat permissions.
- Updated components to utilize the new permission logic, allowing conditional rendering of edit options.
- Adjusted `ChatInput` to display a view-only message when the user lacks edit permissions.
- Cleaned up `package.json` formatting and added missing exports in context files.
This commit is contained in:
dal 2025-09-15 16:50:58 -06:00
parent eff58a11f1
commit b1b21e30a4
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
8 changed files with 89 additions and 37 deletions

View File

@ -4,7 +4,7 @@ import type { IBusterChat } from '@/api/asset_interfaces';
import { useGetChat } from '@/api/buster_rest/chats';
import { Dropdown, type IDropdownItems } from '@/components/ui/dropdown';
import { useGetChatId } from '@/context/Chats/useGetChatId';
import { getIsEffectiveOwner } from '@/lib/share';
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
import {
useDeleteChatSelectMenu,
useDuplicateChatSelectMenu,
@ -32,6 +32,7 @@ export const ChatContainerHeaderDropdown: React.FC<{
const deleteChat = useDeleteChatSelectMenu({ chatId });
const isOwnerEffective = getIsEffectiveOwner(permission);
const canEditChat = canEdit(permission);
const menuItem: IDropdownItems = useMemo(() => {
return [
@ -40,10 +41,20 @@ export const ChatContainerHeaderDropdown: React.FC<{
favoriteChat,
openInNewTab,
{ type: 'divider' },
duplicateChat,
deleteChat,
canEditChat && duplicateChat,
canEditChat && deleteChat,
].filter(Boolean) as IDropdownItems;
}, [chatId, duplicateChat, deleteChat, duplicateChat]);
}, [
chatId,
isOwnerEffective,
canEditChat,
shareMenu,
renameChatTitle,
favoriteChat,
openInNewTab,
duplicateChat,
deleteChat,
]);
return (
<Dropdown align="end" items={menuItem}>

View File

@ -1,2 +1,3 @@
export * from './useChatPermission';
export * from './useGetActiveChat';
export * from './useIsStreamingMessage';

View File

@ -0,0 +1,17 @@
import type { ShareRole } from '@buster/server-shared/share';
import type { IBusterChat } from '../../api/asset_interfaces';
import { useGetChat } from '../../api/buster_rest/chats';
const stablePermissionSelector = (chat: IBusterChat): ShareRole => chat.permission;
export const useChatPermission = (chatId: string | undefined): ShareRole | undefined => {
const { data: permission } = useGetChat(
{ id: chatId || '' },
{
select: stablePermissionSelector,
enabled: !!chatId,
}
);
return permission;
};

View File

@ -1,8 +1,11 @@
import React, { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
import { InputTextAreaButton } from '@/components/ui/inputs/InputTextAreaButton';
import { useIsStreamingMessage } from '@/context/Chats/useIsStreamingMessage';
import { Text } from '@/components/ui/typography';
import { useChatPermission, useIsStreamingMessage } from '@/context/Chats';
import { useGetChatId } from '@/context/Chats/useGetChatId';
import { useIsChatMode } from '@/context/Chats/useMode';
import { cn } from '@/lib/classMerge';
import { canEdit } from '@/lib/share';
import { inputHasText } from '@/lib/text';
import { useChatInputFlow } from '../../../../context/Chats/useChatInputFlow';
import { AIWarning } from './AIWarning';
@ -11,12 +14,15 @@ export const ChatInput: React.FC = React.memo(() => {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const isStreamingMessage = useIsStreamingMessage();
const hasChat = useIsChatMode();
const chatId = useGetChatId();
const permission = useChatPermission(chatId);
const canEditChat = canEdit(permission);
const [inputValue, setInputValue] = useState('');
const disableSubmit = useMemo(() => {
return !inputHasText(inputValue) && !isStreamingMessage;
}, [inputValue, isStreamingMessage]);
return (!inputHasText(inputValue) && !isStreamingMessage) || !canEditChat;
}, [inputValue, isStreamingMessage, canEditChat]);
const { onSubmitPreflight, onStopChat } = useChatInputFlow({
disableSubmit,
@ -44,22 +50,31 @@ export const ChatInput: React.FC = React.memo(() => {
'z-10 mx-3 mt-0.5 mb-2 flex min-h-fit flex-col items-center space-y-1.5 overflow-visible'
)}
>
<InputTextAreaButton
placeholder="Ask Buster a question..."
minRows={2}
maxRows={16}
onSubmit={onSubmitPreflight}
onChange={onChange}
onStop={onStopChat}
loading={isStreamingMessage}
value={inputValue}
disabled={!hasChat}
disabledSubmit={disableSubmit}
autoFocus
ref={textAreaRef}
/>
<AIWarning />
{!canEditChat ? (
<div className="w-full p-4 bg-muted/50 rounded-lg border">
<Text variant="secondary" size="sm" className="text-center">
This chat is view-only. You don't have permission to send messages.
</Text>
</div>
) : (
<>
<InputTextAreaButton
placeholder="Ask Buster a question..."
minRows={2}
maxRows={16}
onSubmit={onSubmitPreflight}
onChange={onChange}
onStop={onStopChat}
loading={isStreamingMessage}
value={inputValue}
disabled={!hasChat || !canEditChat}
disabledSubmit={disableSubmit}
autoFocus
ref={textAreaRef}
/>
<AIWarning />
</>
)}
</div>
);
});

View File

@ -12,8 +12,10 @@ import { ThumbsDown as ThumbsDownFilled } from '@/components/ui/icons/NucleoIcon
import { AppTooltip } from '@/components/ui/tooltip';
import { Text } from '@/components/ui/typography';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useChatPermission } from '@/context/Chats';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { formatDate } from '@/lib/date';
import { canEdit } from '@/lib/share';
import { timeout } from '@/lib/timeout';
export const ChatMessageOptions: React.FC<{
@ -24,6 +26,8 @@ export const ChatMessageOptions: React.FC<{
const { openConfirmModal } = useBusterNotifications();
const { mutateAsync: duplicateChat, isPending: isCopying } = useDuplicateChat();
const { mutateAsync: updateChatMessageFeedback } = useUpdateChatMessageFeedback();
const permission = useChatPermission(chatId);
const canEditChat = canEdit(permission);
const { data: feedback } = useGetChatMessage(messageId, {
select: ({ feedback }) => feedback,
});
@ -65,14 +69,16 @@ export const ChatMessageOptions: React.FC<{
return (
<div className="flex items-center gap-1">
<AppTooltip title="Duplicate chat from this message">
<Button
variant="ghost"
prefix={<DuplicatePlus />}
loading={isCopying}
onClick={warnBeforeDuplicate}
/>
</AppTooltip>
{canEditChat && (
<AppTooltip title="Duplicate chat from this message">
<Button
variant="ghost"
prefix={<DuplicatePlus />}
loading={isCopying}
onClick={warnBeforeDuplicate}
/>
</AppTooltip>
)}
<AppTooltip title="Report message">
<Button
variant="ghost"

View File

@ -7,9 +7,11 @@ import { InputTextArea } from '@/components/ui/inputs/InputTextArea';
import { Tooltip } from '@/components/ui/tooltip';
import { Paragraph } from '@/components/ui/typography';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useChatPermission } from '@/context/Chats';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { useMount } from '@/hooks/useMount';
import { cn } from '@/lib/classMerge';
import { canEdit } from '@/lib/share';
import { useChat } from '../../../context/Chats/useChat';
import { MessageContainer } from './MessageContainer';
@ -22,6 +24,8 @@ export const ChatUserMessage: React.FC<{
const { openSuccessMessage } = useBusterNotifications();
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const permission = useChatPermission(chatId);
const canEditChat = canEdit(permission);
const { sender_avatar, sender_id, sender_name, request } = requestMessage;
@ -67,7 +71,7 @@ export const ChatUserMessage: React.FC<{
{request}
</Paragraph>
</div>
{isStreamFinished && (
{isStreamFinished && canEditChat && (
<RequestMessageTooltip
isTooltipOpen={isTooltipOpen}
setIsEditing={setIsEditing}

View File

@ -66,12 +66,10 @@
"packageManager": "pnpm@10.15.1",
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"shiki"
],
"ignoreMissing": ["shiki"],
"allowedVersions": {
"shiki": "3"
}
}
}
}
}

View File

@ -1,5 +1,5 @@
import { and, eq, isNull } from 'drizzle-orm';
import { type InferSelectModel } from 'drizzle-orm';
import type { InferSelectModel } from 'drizzle-orm';
import { z } from 'zod';
import { db } from '../../connection';
import { reportFiles } from '../../schema';