mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Assorted fixes around approve changes UI (#45)
This commit is contained in:
@@ -9,17 +9,19 @@ export interface RejectChangeData {
|
||||
}
|
||||
|
||||
interface ApproveChangeProps {
|
||||
rejectFormOpen: boolean;
|
||||
setRejectFormOpen: (rejectFormOpen: boolean) => void;
|
||||
onApprove: () => void;
|
||||
onReject: (data: RejectChangeData) => void;
|
||||
}
|
||||
|
||||
const ApproveChange: React.FC<ApproveChangeProps> = ({ onApprove, onReject }) => {
|
||||
const ApproveChange: React.FC<ApproveChangeProps> = ({ rejectFormOpen, setRejectFormOpen, onApprove, onReject }) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [hasRejected, setHasRejected] = useState(false);
|
||||
const [shareProject, setShareProject] = useState(false);
|
||||
|
||||
if (hasRejected) {
|
||||
if (rejectFormOpen) {
|
||||
const performReject = (retry: boolean) => {
|
||||
setRejectFormOpen(false);
|
||||
const explanation = textareaRef.current?.value ?? '';
|
||||
onReject({
|
||||
explanation,
|
||||
@@ -32,7 +34,7 @@ const ApproveChange: React.FC<ApproveChangeProps> = ({ onApprove, onReject }) =>
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg bg-red-50 mt-3',
|
||||
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg bg-red-50 mb-2',
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
@@ -78,16 +80,16 @@ const ApproveChange: React.FC<ApproveChangeProps> = ({ onApprove, onReject }) =>
|
||||
<button
|
||||
onClick={() => performReject(false)}
|
||||
className="flex-1 h-[30px] flex justify-center items-center bg-red-100 border border-red-500 text-red-500 hover:bg-red-200 hover:text-red-600 transition-colors rounded"
|
||||
aria-label="Cancel changes"
|
||||
title="Cancel changes"
|
||||
aria-label="Revert changes"
|
||||
title="Revert changes"
|
||||
>
|
||||
<div className="i-ph:x-bold"></div>
|
||||
<div className="i-ph:arrow-arc-left-bold"></div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => performReject(true)}
|
||||
className="flex-1 h-[30px] flex justify-center items-center bg-green-100 border border-green-500 text-green-500 hover:bg-green-200 hover:text-green-600 transition-colors rounded"
|
||||
aria-label="Try changes again"
|
||||
title="Try changes again"
|
||||
aria-label="Retry changes"
|
||||
title="Retry changes"
|
||||
>
|
||||
<div className="i-ph:repeat-bold"></div>
|
||||
</button>
|
||||
@@ -97,9 +99,9 @@ const ApproveChange: React.FC<ApproveChangeProps> = ({ onApprove, onReject }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 w-full h-[30px] pt-2">
|
||||
<div className="flex items-center gap-1 w-full h-[30px] mb-2">
|
||||
<button
|
||||
onClick={() => setHasRejected(true)}
|
||||
onClick={() => setRejectFormOpen(true)}
|
||||
className="flex-1 h-[30px] flex justify-center items-center bg-red-100 border border-red-500 text-red-500 hover:bg-red-200 hover:text-red-600 transition-colors rounded"
|
||||
aria-label="Reject change"
|
||||
title="Reject change"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Menu } from '~/components/sidebar/Menu.client';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { Workbench } from '~/components/workbench/Workbench.client';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { Messages } from './Messages.client';
|
||||
import { getLastMessageProjectContents, hasFileModifications, Messages } from './Messages.client';
|
||||
import { SendButton } from './SendButton.client';
|
||||
import { APIKeyManager } from './APIKeyManager';
|
||||
import Cookies from 'js-cookie';
|
||||
@@ -27,6 +27,8 @@ import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { RejectChangeData } from './ApproveChange';
|
||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||
import ApproveChange from './ApproveChange';
|
||||
|
||||
export const TEXTAREA_MIN_HEIGHT = 76;
|
||||
|
||||
@@ -93,6 +95,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const [rejectFormOpen, setRejectFormOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(transcript);
|
||||
@@ -218,6 +221,149 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const showApproveChange = (() => {
|
||||
if (isStreaming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!messages?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastMessageProjectContents = getLastMessageProjectContents(messages, messages.length - 1);
|
||||
if (!lastMessageProjectContents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastMessageProjectContents.contentsMessageId != approveChangesMessageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (!hasFileModifications(lastMessage.content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
let messageInput;
|
||||
if (!rejectFormOpen) {
|
||||
messageInput = (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={classNames(
|
||||
'w-full pl-4 pt-4 pr-25 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
|
||||
'transition-all duration-200',
|
||||
'hover:border-bolt-elements-focus',
|
||||
)}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
files.forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
const base64Image = e.target?.result as string;
|
||||
setUploadedFiles?.([...uploadedFiles, file]);
|
||||
setImageDataList?.([...imageDataList, base64Image]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (isStreaming) {
|
||||
handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore if using input method engine
|
||||
if (event.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
value={input}
|
||||
onChange={(event) => {
|
||||
handleInputChange?.(event);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
style={{
|
||||
minHeight: TEXTAREA_MIN_HEIGHT,
|
||||
maxHeight: TEXTAREA_MAX_HEIGHT,
|
||||
}}
|
||||
placeholder={chatStarted ? "How can we help you?" : "What do you want to build?"}
|
||||
translate="no"
|
||||
/>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<SendButton
|
||||
show={(input.length > 0 || uploadedFiles.length > 0) && chatStarted}
|
||||
isStreaming={isStreaming}
|
||||
onClick={(event) => {
|
||||
if (input.length > 0 || uploadedFiles.length > 0) {
|
||||
handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div className="flex justify-between items-center text-sm p-4 pt-2">
|
||||
<div className="flex gap-1 items-center">
|
||||
<IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
|
||||
<div className="i-ph:paperclip text-xl"></div>
|
||||
</IconButton>
|
||||
|
||||
<SpeechRecognitionButton
|
||||
isListening={isListening}
|
||||
onStart={startListening}
|
||||
onStop={stopListening}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
|
||||
</div>
|
||||
{input.length > 3 ? (
|
||||
<div className="text-xs text-bolt-elements-textTertiary">
|
||||
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
|
||||
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a
|
||||
new line
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const baseChat = (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -251,9 +397,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
messages={messages}
|
||||
isStreaming={isStreaming}
|
||||
onRewind={onRewind}
|
||||
approveChangesMessageId={approveChangesMessageId}
|
||||
onApproveChange={onApproveChange}
|
||||
onRejectChange={onRejectChange}
|
||||
/>
|
||||
) : null;
|
||||
}}
|
||||
@@ -310,117 +453,31 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={classNames(
|
||||
'w-full pl-4 pt-4 pr-25 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
|
||||
'transition-all duration-200',
|
||||
'hover:border-bolt-elements-focus',
|
||||
)}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
files.forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
const base64Image = e.target?.result as string;
|
||||
setUploadedFiles?.([...uploadedFiles, file]);
|
||||
setImageDataList?.([...imageDataList, base64Image]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (isStreaming) {
|
||||
handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore if using input method engine
|
||||
if (event.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSendMessage?.(event);
|
||||
{showApproveChange && (
|
||||
<ApproveChange
|
||||
rejectFormOpen={rejectFormOpen}
|
||||
setRejectFormOpen={setRejectFormOpen}
|
||||
onApprove={() => {
|
||||
if (onApproveChange && messages) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
assert(lastMessage);
|
||||
onApproveChange(lastMessage.id);
|
||||
}
|
||||
}}
|
||||
value={input}
|
||||
onChange={(event) => {
|
||||
handleInputChange?.(event);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
style={{
|
||||
minHeight: TEXTAREA_MIN_HEIGHT,
|
||||
maxHeight: TEXTAREA_MAX_HEIGHT,
|
||||
}}
|
||||
placeholder={chatStarted ? "How can we help you?" : "What do you want to build?"}
|
||||
translate="no"
|
||||
/>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<SendButton
|
||||
show={(input.length > 0 || uploadedFiles.length > 0) && chatStarted}
|
||||
isStreaming={isStreaming}
|
||||
onClick={(event) => {
|
||||
if (input.length > 0 || uploadedFiles.length > 0) {
|
||||
handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div className="flex justify-between items-center text-sm p-4 pt-2">
|
||||
<div className="flex gap-1 items-center">
|
||||
<IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
|
||||
<div className="i-ph:paperclip text-xl"></div>
|
||||
</IconButton>
|
||||
onReject={(data) => {
|
||||
if (onRejectChange && messages) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
assert(lastMessage);
|
||||
|
||||
<SpeechRecognitionButton
|
||||
isListening={isListening}
|
||||
onStart={startListening}
|
||||
onStop={stopListening}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
|
||||
</div>
|
||||
{input.length > 3 ? (
|
||||
<div className="text-xs text-bolt-elements-textTertiary">
|
||||
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
|
||||
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a
|
||||
new line
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
const info = getLastMessageProjectContents(messages, messages.length - 1);
|
||||
assert(info);
|
||||
|
||||
onRejectChange(lastMessage.id, info.rewindMessageId, info.contents.content, data);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!rejectFormOpen && messageInput}
|
||||
</div>
|
||||
</div>
|
||||
{!chatStarted && (
|
||||
|
||||
@@ -323,7 +323,7 @@ export const ChatImpl = memo(
|
||||
}
|
||||
|
||||
const enhancedPromptMessage: Message = {
|
||||
id: `enhanced-prompt-${messages.length}`,
|
||||
id: `enhanced-prompt-${Math.random()}`,
|
||||
role: 'assistant',
|
||||
content: message,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { Message } from 'ai';
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { AssistantMessage, getAnnotationsTokensUsage } from './AssistantMessage';
|
||||
import { AssistantMessage } from './AssistantMessage';
|
||||
import { UserMessage } from './UserMessage';
|
||||
import { useLocation } from '@remix-run/react';
|
||||
import { forkChat } from '~/lib/persistence/db';
|
||||
import { toast } from 'react-toastify';
|
||||
import WithTooltip from '~/components/ui/Tooltip';
|
||||
import { assert, sendCommandDedicatedClient } from '~/lib/replay/ReplayProtocolClient';
|
||||
import ApproveChange, { type RejectChangeData } from './ApproveChange';
|
||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||
|
||||
interface MessagesProps {
|
||||
id?: string;
|
||||
@@ -16,9 +12,6 @@ interface MessagesProps {
|
||||
isStreaming?: boolean;
|
||||
messages?: Message[];
|
||||
onRewind?: (messageId: string, contents: string) => void;
|
||||
approveChangesMessageId?: string;
|
||||
onApproveChange?: (messageId: string) => void;
|
||||
onRejectChange?: (lastMessageId: string, rewindMessageId: string, contents: string, data: RejectChangeData) => void;
|
||||
}
|
||||
|
||||
interface ProjectContents {
|
||||
@@ -31,55 +24,33 @@ export function saveProjectContents(messageId: string, contents: ProjectContents
|
||||
gProjectContentsByMessageId.set(messageId, contents);
|
||||
}
|
||||
|
||||
function hasFileModifications(content: string) {
|
||||
export function getLastMessageProjectContents(messages: Message[], index: number) {
|
||||
// The message index is for the model response, and the project
|
||||
// contents will be associated with the last message present when
|
||||
// the user prompt was sent to the model. This could be either two
|
||||
// or three messages back, depending on whether a bug explanation was added.
|
||||
const beforeUserMessage = messages[index - 2];
|
||||
const contents = gProjectContentsByMessageId.get(beforeUserMessage?.id);
|
||||
if (!contents) {
|
||||
const priorMessage = messages[index - 3];
|
||||
const priorContents = gProjectContentsByMessageId.get(priorMessage?.id);
|
||||
if (!priorContents) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We still rewind to just before the user message to retain any
|
||||
// explanation from the Nut API.
|
||||
return { rewindMessageId: beforeUserMessage.id, contentsMessageId: priorMessage.id, contents: priorContents };
|
||||
}
|
||||
return { rewindMessageId: beforeUserMessage.id, contentsMessageId: beforeUserMessage.id, contents };
|
||||
}
|
||||
|
||||
export function hasFileModifications(content: string) {
|
||||
return content.includes('__boltArtifact__');
|
||||
}
|
||||
|
||||
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
||||
const { id, isStreaming = false, messages = [], onRewind, approveChangesMessageId, onApproveChange, onRejectChange } = props;
|
||||
|
||||
const getLastMessageProjectContents = (index: number) => {
|
||||
// The message index is for the model response, and the project
|
||||
// contents will be associated with the last message present when
|
||||
// the user prompt was sent to the model. This could be either two
|
||||
// or three messages back, depending on whether a bug explanation was added.
|
||||
const beforeUserMessage = messages[index - 2];
|
||||
const contents = gProjectContentsByMessageId.get(beforeUserMessage?.id);
|
||||
if (!contents) {
|
||||
const priorMessage = messages[index - 3];
|
||||
const priorContents = gProjectContentsByMessageId.get(priorMessage?.id);
|
||||
if (!priorContents) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We still rewind to just before the user message to retain any
|
||||
// explanation from the Nut API.
|
||||
return { messageId: beforeUserMessage.id, contents: priorContents };
|
||||
}
|
||||
return { messageId: beforeUserMessage.id, contents };
|
||||
};
|
||||
|
||||
const showApproveChange = (() => {
|
||||
if (isStreaming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!messages.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastMessageProjectContents = getLastMessageProjectContents(messages.length - 1);
|
||||
if (!lastMessageProjectContents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastMessageProjectContents.messageId != approveChangesMessageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
return hasFileModifications(lastMessage.content);
|
||||
})();
|
||||
const { id, isStreaming = false, messages = [], onRewind } = props;
|
||||
|
||||
return (
|
||||
<div id={id} ref={ref} className={props.className}>
|
||||
@@ -121,15 +92,15 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
{!isUserMessage &&
|
||||
messageId &&
|
||||
onRewind &&
|
||||
getLastMessageProjectContents(index) &&
|
||||
getLastMessageProjectContents(messages, index) &&
|
||||
hasFileModifications(content) && (
|
||||
<div className="flex gap-2 flex-col lg:flex-row">
|
||||
<WithTooltip tooltip="Undo changes in this message">
|
||||
<button
|
||||
onClick={() => {
|
||||
const info = getLastMessageProjectContents(index);
|
||||
const info = getLastMessageProjectContents(messages, index);
|
||||
assert(info);
|
||||
onRewind(info.messageId, info.contents.content);
|
||||
onRewind(info.rewindMessageId, info.contents.content);
|
||||
}}
|
||||
key="i-ph:arrow-u-up-left"
|
||||
className={classNames(
|
||||
@@ -148,28 +119,6 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
{isStreaming && (
|
||||
<div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
|
||||
)}
|
||||
{showApproveChange && (
|
||||
<ApproveChange
|
||||
onApprove={() => {
|
||||
if (onApproveChange) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
assert(lastMessage);
|
||||
onApproveChange(lastMessage.id);
|
||||
}
|
||||
}}
|
||||
onReject={(data) => {
|
||||
if (onRejectChange) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
assert(lastMessage);
|
||||
|
||||
const info = getLastMessageProjectContents(messages.length - 1);
|
||||
assert(info);
|
||||
|
||||
onRejectChange(lastMessage.id, info.messageId, info.contents.content, data);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function submitProblem(problem: BoltProblemInput): Promise<string |
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProblem(problemId: string, problem: BoltProblemInput) {
|
||||
export async function updateProblem(problemId: string, problem: BoltProblemInput | undefined) {
|
||||
try {
|
||||
if (!getNutIsAdmin()) {
|
||||
toast.error("Admin user required");
|
||||
|
||||
@@ -12,16 +12,16 @@ import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
|
||||
|
||||
function Comments({ comments }: { comments: BoltProblemComment[] }) {
|
||||
return (
|
||||
<div className="comments">
|
||||
<div className="space-y-4 mt-6">
|
||||
{comments.map((comment, index) => (
|
||||
<div key={index} className="comment">
|
||||
<div className="comment-header">
|
||||
<span className="comment-username">{comment.username ?? ""}</span>
|
||||
<span className="comment-date">
|
||||
<div key={index} className="bg-bolt-elements-background-depth-2 rounded-lg p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-bolt-text">{comment.username ?? "Anonymous"}</span>
|
||||
<span className="text-sm text-bolt-text-secondary">
|
||||
{new Date(comment.timestamp).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="comment-text">{comment.content}</div>
|
||||
<div className="text-bolt-text whitespace-pre-wrap">{comment.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -89,8 +89,9 @@ function UpdateProblemForm(props: UpdateProblemFormProps) {
|
||||
|
||||
type DoUpdateCallback = (problem: BoltProblem) => BoltProblem;
|
||||
type UpdateProblemCallback = (doUpdate: DoUpdateCallback) => void;
|
||||
type DeleteProblemCallback = () => void;
|
||||
|
||||
function UpdateProblemForms({ updateProblem }: { updateProblem: UpdateProblemCallback }) {
|
||||
function UpdateProblemForms({ updateProblem, deleteProblem }: { updateProblem: UpdateProblemCallback, deleteProblem: DeleteProblemCallback }) {
|
||||
const handleAddComment = (content: string) => {
|
||||
const newComment: BoltProblemComment = {
|
||||
timestamp: Date.now(),
|
||||
@@ -147,6 +148,15 @@ function UpdateProblemForms({ updateProblem }: { updateProblem: UpdateProblemCal
|
||||
<UpdateProblemForm handleSubmit={handleSetDescription} updateText="Set Description" placeholder="Set the description of the problem..." />
|
||||
<UpdateProblemForm handleSubmit={handleSetStatus} updateText="Set Status" placeholder="Set the status of the problem..." />
|
||||
<UpdateProblemForm handleSubmit={handleSetKeywords} updateText="Set Keywords" placeholder="Set the keywords of the problem..." />
|
||||
|
||||
<div className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
|
||||
<button
|
||||
onClick={deleteProblem}
|
||||
className="px-6 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors duration-200 font-medium"
|
||||
>
|
||||
Delete Problem
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -173,6 +183,12 @@ function ViewProblemPage() {
|
||||
await backendUpdateProblem(problemId, newProblem);
|
||||
}, [problemData]);
|
||||
|
||||
const deleteProblem = useCallback(async () => {
|
||||
console.log("BackendDeleteProblem", problemId);
|
||||
await backendUpdateProblem(problemId, undefined);
|
||||
toast.success("Problem deleted");
|
||||
}, [problemData]);
|
||||
|
||||
useEffect(() => {
|
||||
getProblem(problemId).then(setProblemData);
|
||||
}, [problemId]);
|
||||
@@ -193,7 +209,7 @@ function ViewProblemPage() {
|
||||
: <ProblemViewer problem={problemData} />}
|
||||
</div>
|
||||
{getNutIsAdmin() && problemData && (
|
||||
<UpdateProblemForms updateProblem={updateProblem} />
|
||||
<UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />
|
||||
)}
|
||||
<ToastContainerWrapper />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user