Skip to content

Commit bb5e9d1

Browse files
committed
Allow editing file attachments when editing messages.
1 parent f7c9429 commit bb5e9d1

File tree

4 files changed

+110
-34
lines changed

4 files changed

+110
-34
lines changed

tools/server/public/index.html.gz

-47 Bytes
Binary file not shown.

tools/server/webui/src/components/ChatMessage.tsx

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import { useMemo, useState } from 'react';
1+
import { useMemo, useState, ClipboardEvent } from 'react';
22
import { useAppContext } from '../utils/app.context';
3-
import { Message, PendingMessage } from '../utils/types';
3+
import { Message, MessageExtra, PendingMessage } from '../utils/types';
44
import { classNames } from '../utils/misc';
55
import MarkdownDisplay, { CopyButton } from './MarkdownDisplay';
66
import {
77
ArrowPathIcon,
88
ChevronLeftIcon,
99
ChevronRightIcon,
10+
PaperClipIcon,
1011
PencilSquareIcon,
1112
} from '@heroicons/react/24/outline';
1213
import ChatInputExtraContextItem from './ChatInputExtraContextItem';
1314
import { BtnWithTooltips } from '../utils/common';
15+
import Dropzone from 'react-dropzone';
16+
import { useChatExtraContext } from './useChatExtraContext';
1417

1518
interface SplitMessage {
1619
content: PendingMessage['content'];
@@ -33,12 +36,18 @@ export default function ChatMessage({
3336
siblingCurrIdx: number;
3437
id?: string;
3538
onRegenerateMessage(msg: Message): void;
36-
onEditMessage(msg: Message, content: string): void;
39+
onEditMessage(
40+
msg: Message,
41+
content: string,
42+
extra: MessageExtra[] | undefined
43+
): void;
3744
onChangeSibling(sibling: Message['id']): void;
3845
isPending?: boolean;
3946
}) {
4047
const { viewingChat, config } = useAppContext();
48+
const extraContext = useChatExtraContext(msg.extra ?? []);
4149
const [editingContent, setEditingContent] = useState<string | null>(null);
50+
const [isDrag, setIsDrag] = useState(false);
4251
const timings = useMemo(
4352
() =>
4453
msg.timings
@@ -107,36 +116,92 @@ export default function ChatMessage({
107116
className={classNames({
108117
'chat-bubble markdown': true,
109118
'chat-bubble bg-transparent': !isUser,
119+
'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted
110120
})}
111121
>
112122
{/* textarea for editing message */}
113123
{editingContent !== null && (
114-
<>
115-
<textarea
116-
dir="auto"
117-
className="textarea textarea-bordered bg-base-100 text-base-content max-w-2xl w-[calc(90vw-8em)] h-24"
118-
value={editingContent}
119-
onChange={(e) => setEditingContent(e.target.value)}
120-
></textarea>
121-
<br />
122-
<button
123-
className="btn btn-ghost mt-2 mr-2"
124-
onClick={() => setEditingContent(null)}
125-
>
126-
Cancel
127-
</button>
128-
<button
129-
className="btn mt-2"
130-
onClick={() => {
131-
if (msg.content !== null) {
132-
setEditingContent(null);
133-
onEditMessage(msg as Message, editingContent);
134-
}
135-
}}
136-
>
137-
Submit
138-
</button>
139-
</>
124+
<Dropzone
125+
noClick
126+
onDrop={(files: File[]) => {
127+
setIsDrag(false);
128+
extraContext.onFileAdded(files);
129+
}}
130+
onDragEnter={() => setIsDrag(true)}
131+
onDragLeave={() => setIsDrag(false)}
132+
multiple={true}
133+
>
134+
{({ getRootProps, getInputProps }) => (
135+
<div
136+
className="flex flex-col w-full"
137+
onPasteCapture={(e: ClipboardEvent<HTMLInputElement>) => {
138+
const files = Array.from(e.clipboardData.items)
139+
.filter((item) => item.kind === 'file')
140+
.map((item) => item.getAsFile())
141+
.filter((file) => file !== null);
142+
143+
if (files.length > 0) {
144+
e.preventDefault();
145+
extraContext.onFileAdded(files);
146+
}
147+
}}
148+
{...getRootProps()}
149+
>
150+
<ChatInputExtraContextItem
151+
items={extraContext.items}
152+
removeItem={extraContext.removeItem}
153+
/>
154+
155+
<div className="flex flex-row gap-2 ml-2">
156+
<textarea
157+
dir="auto"
158+
className="textarea textarea-bordered bg-base-100 text-base-content max-w-2xl w-[calc(90vw-8em)] h-24"
159+
value={editingContent}
160+
onChange={(e) => setEditingContent(e.target.value)}
161+
/>
162+
<label
163+
htmlFor="file-upload"
164+
className={classNames({
165+
'btn w-8 h-8 p-0 rounded-full': true,
166+
})}
167+
>
168+
<PaperClipIcon className="h-5 w-5" />
169+
</label>
170+
<input
171+
id="file-upload"
172+
type="file"
173+
className="hidden"
174+
{...getInputProps()}
175+
hidden
176+
/>
177+
</div>
178+
179+
<div className="flex flex-row gap-2 ml-2">
180+
<button
181+
className="btn btn-ghost mt-2 mr-2"
182+
onClick={() => setEditingContent(null)}
183+
>
184+
Cancel
185+
</button>
186+
<button
187+
className="btn mt-2"
188+
onClick={() => {
189+
if (msg.content !== null) {
190+
setEditingContent(null);
191+
onEditMessage(
192+
msg as Message,
193+
editingContent,
194+
extraContext.items
195+
);
196+
}
197+
}}
198+
>
199+
Submit
200+
</button>
201+
</div>
202+
</div>
203+
)}
204+
</Dropzone>
140205
)}
141206
{/* not editing content, render message */}
142207
{editingContent === null && (

tools/server/webui/src/components/ChatScreen.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { ClipboardEvent, useEffect, useMemo, useRef, useState } from 'react';
22
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
33
import ChatMessage from './ChatMessage';
4-
import { CanvasType, Message, PendingMessage } from '../utils/types';
4+
import {
5+
CanvasType,
6+
Message,
7+
MessageExtra,
8+
PendingMessage,
9+
} from '../utils/types';
510
import { classNames, cleanCurrentUrl } from '../utils/misc';
611
import CanvasPyInterpreter from './CanvasPyInterpreter';
712
import StorageUtils from '../utils/storage';
@@ -158,15 +163,19 @@ export default function ChatScreen() {
158163
// for vscode context
159164
textarea.refOnSubmit.current = sendNewMessage;
160165

161-
const handleEditMessage = async (msg: Message, content: string) => {
166+
const handleEditMessage = async (
167+
msg: Message,
168+
content: string,
169+
extra: MessageExtra[] | undefined
170+
) => {
162171
if (!viewingChat) return;
163172
setCurrNodeId(msg.id);
164173
scrollToBottom(false);
165174
await replaceMessageAndGenerate(
166175
viewingChat.conv.id,
167176
msg.parent,
168177
content,
169-
msg.extra,
178+
extra,
170179
onChunk
171180
);
172181
setCurrNodeId(-1);

tools/server/webui/src/components/useChatExtraContext.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ export interface ChatExtraContextApi {
2323
onFileAdded: (files: File[]) => void; // used by "upload" button
2424
}
2525

26-
export function useChatExtraContext(): ChatExtraContextApi {
26+
export function useChatExtraContext(
27+
initialItems: MessageExtra[] = []
28+
): ChatExtraContextApi {
2729
const { serverProps, config } = useAppContext();
28-
const [items, setItems] = useState<MessageExtra[]>([]);
30+
const [items, setItems] = useState<MessageExtra[]>(initialItems);
2931

3032
const addItems = (newItems: MessageExtra[]) => {
3133
setItems((prev) => [...prev, ...newItems]);

0 commit comments

Comments
 (0)