Skip to content

Commit f4af3f3

Browse files
committed
rework the input area
1 parent 141a908 commit f4af3f3

File tree

10 files changed

+334
-117
lines changed

10 files changed

+334
-117
lines changed

tools/server/webui/package-lock.json

Lines changed: 92 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/server/webui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"postcss": "^8.4.49",
2525
"react": "^18.3.1",
2626
"react-dom": "^18.3.1",
27+
"react-dropzone": "^14.3.8",
28+
"react-hot-toast": "^2.5.2",
2729
"react-markdown": "^9.0.3",
2830
"react-router": "^7.1.5",
2931
"rehype-highlight": "^7.0.2",

tools/server/webui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Sidebar from './components/Sidebar';
44
import { AppContextProvider, useAppContext } from './utils/app.context';
55
import ChatScreen from './components/ChatScreen';
66
import SettingDialog from './components/SettingDialog';
7+
import { Toaster } from 'react-hot-toast';
78

89
function App() {
910
return (
@@ -40,6 +41,7 @@ function AppLayout() {
4041
onClose={() => setShowSettings(false)}
4142
/>
4243
}
44+
<Toaster />
4345
</>
4446
);
4547
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export default function ChatMessage({
8888
<div
8989
className={classNames({
9090
'chat-bubble markdown': true,
91-
'chat-bubble-base-300': msg.role !== 'user',
91+
'chat-bubble bg-transparent': msg.role !== 'user',
9292
})}
9393
>
9494
{/* textarea for editing message */}

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

Lines changed: 124 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import { useEffect, useMemo, 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 { CanvasType, Message, MessageExtraContext, PendingMessage } from '../utils/types';
55
import { classNames, cleanCurrentUrl, throttle } from '../utils/misc';
66
import CanvasPyInterpreter from './CanvasPyInterpreter';
77
import StorageUtils from '../utils/storage';
88
import { useVSCodeContext } from '../utils/llama-vscode';
99
import { useChatTextarea, ChatTextareaApi } from './useChatTextarea.ts';
10+
import {
11+
ArrowUpIcon,
12+
StopIcon,
13+
PaperClipIcon,
14+
DocumentTextIcon,
15+
} from '@heroicons/react/24/solid';
16+
import {
17+
ChatExtraContextApi,
18+
useChatExtraContext,
19+
} from './useChatExtraContext.tsx';
20+
import Dropzone from 'react-dropzone';
1021

1122
/**
1223
* A message display is a message node with additional information for rendering.
@@ -102,10 +113,8 @@ export default function ChatScreen() {
102113
} = useAppContext();
103114

104115
const textarea: ChatTextareaApi = useChatTextarea(prefilledMsg.content());
105-
106-
const { extraContext, clearExtraContext } = useVSCodeContext(textarea);
107-
// TODO: improve this when we have "upload file" feature
108-
const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined;
116+
const extraContext = useChatExtraContext();
117+
useVSCodeContext(textarea, extraContext);
109118

110119
// keep track of leaf node for rendering
111120
const [currNodeId, setCurrNodeId] = useState<number>(-1);
@@ -146,15 +155,15 @@ export default function ChatScreen() {
146155
currConvId,
147156
lastMsgNodeId,
148157
lastInpMsg,
149-
currExtra,
158+
extraContext.items,
150159
onChunk
151160
))
152161
) {
153162
// restore the input message if failed
154163
textarea.setValue(lastInpMsg);
155164
}
156165
// OK
157-
clearExtraContext();
166+
extraContext.clearItems();
158167
};
159168

160169
// for vscode context
@@ -253,41 +262,13 @@ export default function ChatScreen() {
253262
</div>
254263

255264
{/* chat input */}
256-
<div className="flex flex-row items-end pt-8 pb-6 sticky bottom-0 bg-base-100">
257-
<textarea
258-
// Default (mobile): Enable vertical resize, overflow auto for scrolling if needed
259-
// Large screens (lg:): Disable manual resize, apply max-height for autosize limit
260-
className="textarea textarea-bordered w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60)
261-
placeholder="Type a message (Shift+Enter to add a new line)"
262-
ref={textarea.ref}
263-
onInput={textarea.onInput} // Hook's input handler (will only resize height on lg+ screens)
264-
onKeyDown={(e) => {
265-
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
266-
if (e.key === 'Enter' && !e.shiftKey) {
267-
e.preventDefault();
268-
sendNewMessage();
269-
}
270-
}}
271-
id="msg-input"
272-
dir="auto"
273-
// Set a base height of 2 rows for mobile views
274-
// On lg+ screens, the hook will calculate and set the initial height anyway
275-
rows={2}
276-
></textarea>
277-
278-
{isGenerating(currConvId ?? '') ? (
279-
<button
280-
className="btn btn-neutral ml-2"
281-
onClick={() => stopGenerating(currConvId ?? '')}
282-
>
283-
Stop
284-
</button>
285-
) : (
286-
<button className="btn btn-primary ml-2" onClick={sendNewMessage}>
287-
Send
288-
</button>
289-
)}
290-
</div>
265+
<ChatInput
266+
textarea={textarea}
267+
extraContext={extraContext}
268+
onSend={sendNewMessage}
269+
onStop={() => stopGenerating(currConvId ?? '')}
270+
isGenerating={isGenerating(currConvId ?? '')}
271+
/>
291272
</div>
292273
<div className="w-full sticky top-[7em] h-[calc(100vh-9em)]">
293274
{canvasData?.type === CanvasType.PY_INTERPRETER && (
@@ -297,3 +278,104 @@ export default function ChatScreen() {
297278
</div>
298279
);
299280
}
281+
282+
function ChatInput({
283+
textarea,
284+
extraContext,
285+
onSend,
286+
onStop,
287+
isGenerating,
288+
}: {
289+
textarea: ChatTextareaApi;
290+
extraContext: ChatExtraContextApi;
291+
onSend: () => void;
292+
onStop: () => void;
293+
isGenerating: boolean;
294+
}) {
295+
const [isDrag, setIsDrag] = useState(false);
296+
297+
return (
298+
<div
299+
className={classNames({
300+
'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100': true,
301+
'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted
302+
})}
303+
>
304+
<Dropzone
305+
noClick
306+
onDrop={(files: File[]) => {
307+
setIsDrag(false);
308+
extraContext.onFileAdded(files);
309+
}}
310+
onDragEnter={() => setIsDrag(true)}
311+
onDragLeave={() => setIsDrag(false)}
312+
multiple={true}
313+
>
314+
{({ getRootProps, getInputProps }) => (
315+
<div className="flex rounded-xl border-1 border-base-content/30 p-3 w-full" {...getRootProps()}>
316+
<textarea
317+
// Default (mobile): Enable vertical resize, overflow auto for scrolling if needed
318+
// Large screens (lg:): Disable manual resize, apply max-height for autosize limit
319+
className="text-md outline-none border-none w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60)
320+
placeholder="Type a message (Shift+Enter to add a new line)"
321+
ref={textarea.ref}
322+
onInput={textarea.onInput} // Hook's input handler (will only resize height on lg+ screens)
323+
onKeyDown={(e) => {
324+
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
325+
if (e.key === 'Enter' && !e.shiftKey) {
326+
e.preventDefault();
327+
onSend();
328+
}
329+
}}
330+
id="msg-input"
331+
dir="auto"
332+
// Set a base height of 2 rows for mobile views
333+
// On lg+ screens, the hook will calculate and set the initial height anyway
334+
rows={2}
335+
></textarea>
336+
337+
{/* buttons area */}
338+
<div className="flex flex-row gap-2 ml-2">
339+
<label
340+
htmlFor="file-upload"
341+
className="btn w-8 h-8 p-0 rounded-full"
342+
>
343+
<PaperClipIcon className="h-5 w-5" />
344+
</label>
345+
<input
346+
id="file-upload"
347+
type="file"
348+
className="hidden"
349+
disabled={isGenerating}
350+
{...getInputProps()}
351+
hidden
352+
/>
353+
{isGenerating ? (
354+
<button
355+
className="btn btn-neutral w-8 h-8 p-0 rounded-full"
356+
onClick={onStop}
357+
>
358+
<StopIcon className="h-5 w-5" />
359+
</button>
360+
) : (
361+
<button
362+
className="btn btn-primary w-8 h-8 p-0 rounded-full"
363+
onClick={onSend}
364+
>
365+
<ArrowUpIcon className="h-5 w-5" />
366+
</button>
367+
)}
368+
</div>
369+
</div>
370+
)}
371+
</Dropzone>
372+
</div>
373+
);
374+
}
375+
376+
function ChatInputExtraContextItem({}: {
377+
idx: number,
378+
item: MessageExtraContext,
379+
}) {
380+
381+
}

0 commit comments

Comments
 (0)