Skip to content

Commit d44c2d3

Browse files
authored
Webui: New Features for Conversations, Settings, and Chat Messages (#618)
* Webui: add Rename/Upload conversation in header and sidebar webui: don't change modified date when renaming conversation * webui: add a preset feature to the settings #14649 * webui: Add editing assistant messages #13522 Webui: keep the following message while editing assistance response. webui: change icon to edit message * webui: DB import and export #14347 * webui: Wrap long numbers instead of infinite horizontal scroll (#14062) fix sidebar being covered by main content #14082 --------- Co-authored-by: firecoperana <firecoperana>
1 parent f989fb0 commit d44c2d3

File tree

17 files changed

+1560
-163
lines changed

17 files changed

+1560
-163
lines changed

examples/server/public/index.html.gz

28.4 KB
Binary file not shown.

examples/server/webui/dist/index.html

Lines changed: 277 additions & 70 deletions
Large diffs are not rendered by default.

examples/server/webui/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
content="width=device-width, initial-scale=1, maximum-scale=1"
88
/>
99
<meta name="color-scheme" content="light dark" />
10-
<title>🦙 llama.cpp - chat</title>
10+
<title>🦙 ik_llama.cpp - chat</title>
1111
</head>
1212
<body>
1313
<div id="root"></div>

examples/server/webui/package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/server/webui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
"autoprefixer": "^10.4.20",
2020
"daisyui": "^5.0.12",
2121
"dexie": "^4.0.11",
22+
"dexie-export-import": "^4.0.11",
2223
"highlight.js": "^11.10.0",
2324
"katex": "^0.16.15",
2425
"postcss": "^8.4.49",
2526
"react": "^18.3.1",
2627
"react-dom": "^18.3.1",
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",

examples/server/webui/src/App.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,24 @@ 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 { ModalProvider } from './components/ModalProvider';
78

89
function App() {
910
return (
10-
<HashRouter>
11-
<div className="flex flex-row drawer lg:drawer-open">
12-
<AppContextProvider>
13-
<Routes>
14-
<Route element={<AppLayout />}>
15-
<Route path="/chat/:convId" element={<ChatScreen />} />
16-
<Route path="*" element={<ChatScreen />} />
17-
</Route>
18-
</Routes>
19-
</AppContextProvider>
20-
</div>
21-
</HashRouter>
11+
<ModalProvider>
12+
<HashRouter>
13+
<div className="flex flex-row drawer lg:drawer-open">
14+
<AppContextProvider>
15+
<Routes>
16+
<Route element={<AppLayout />}>
17+
<Route path="/chat/:convId" element={<ChatScreen />} />
18+
<Route path="*" element={<ChatScreen />} />
19+
</Route>
20+
</Routes>
21+
</AppContextProvider>
22+
</div>
23+
</HashRouter>
24+
</ModalProvider>
2225
);
2326
}
2427

@@ -28,7 +31,7 @@ function AppLayout() {
2831
<>
2932
<Sidebar />
3033
<div
31-
className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto bg-base-100"
34+
className="drawer-content grow flex flex-col h-screen mx-auto px-4 overflow-auto bg-base-100"
3235
id="main-scroll"
3336
>
3437
<Header />

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ export default function ChatMessage({
2020
onEditMessage,
2121
onChangeSibling,
2222
isPending,
23+
onContinueMessage,
2324
}: {
2425
msg: Message | PendingMessage;
2526
siblingLeafNodeIds: Message['id'][];
2627
siblingCurrIdx: number;
2728
id?: string;
2829
onRegenerateMessage(msg: Message): void;
2930
onEditMessage(msg: Message, content: string): void;
31+
onContinueMessage(msg: Message, content: string): void;
3032
onChangeSibling(sibling: Message['id']): void;
3133
isPending?: boolean;
3234
}) {
@@ -112,7 +114,11 @@ export default function ChatMessage({
112114
onClick={() => {
113115
if (msg.content !== null) {
114116
setEditingContent(null);
115-
onEditMessage(msg as Message, editingContent);
117+
if (msg.role === 'user') {
118+
onEditMessage(msg as Message, editingContent);
119+
} else {
120+
onContinueMessage(msg as Message, editingContent);
121+
}
116122
}
117123
}}
118124
>
@@ -283,6 +289,15 @@ export default function ChatMessage({
283289
🔄 Regenerate
284290
</button>
285291
)}
292+
{!isPending && (
293+
<button
294+
className="badge btn-mini show-on-hover"
295+
onClick={() => setEditingContent(msg.content)}
296+
disabled={msg.content === null}
297+
>
298+
✍️ Edit
299+
</button>
300+
)}
286301
</>
287302
)}
288303
<CopyButton

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default function ChatScreen() {
9999
pendingMessages,
100100
canvasData,
101101
replaceMessageAndGenerate,
102+
continueMessageAndGenerate,
102103
} = useAppContext();
103104

104105
const textarea: ChatTextareaApi = useChatTextarea(prefilledMsg.content());
@@ -187,6 +188,20 @@ export default function ChatScreen() {
187188
scrollToBottom(false);
188189
};
189190

191+
const handleContinueMessage = async (msg: Message, content: string) => {
192+
if (!viewingChat || !continueMessageAndGenerate) return;
193+
setCurrNodeId(msg.id);
194+
scrollToBottom(false);
195+
await continueMessageAndGenerate(
196+
viewingChat.conv.id,
197+
msg.id,
198+
content,
199+
onChunk
200+
);
201+
setCurrNodeId(-1);
202+
scrollToBottom(false);
203+
};
204+
190205
const hasCanvas = !!canvasData;
191206

192207
useEffect(() => {
@@ -204,7 +219,7 @@ export default function ChatScreen() {
204219

205220
// due to some timing issues of StorageUtils.appendMsg(), we need to make sure the pendingMsg is not duplicated upon rendering (i.e. appears once in the saved conversation and once in the pendingMsg)
206221
const pendingMsgDisplay: MessageDisplay[] =
207-
pendingMsg && messages.at(-1)?.msg.id !== pendingMsg.id
222+
pendingMsg && !messages.some((m) => m.msg.id === pendingMsg.id) // Only show if pendingMsg is not an existing message being continued
208223
? [
209224
{
210225
msg: pendingMsg,
@@ -236,17 +251,35 @@ export default function ChatScreen() {
236251
{/* placeholder to shift the message to the bottom */}
237252
{viewingChat ? '' : 'Send a message to start'}
238253
</div>
239-
{[...messages, ...pendingMsgDisplay].map((msg) => (
240-
<ChatMessage
241-
key={msg.msg.id}
242-
msg={msg.msg}
243-
siblingLeafNodeIds={msg.siblingLeafNodeIds}
244-
siblingCurrIdx={msg.siblingCurrIdx}
245-
onRegenerateMessage={handleRegenerateMessage}
246-
onEditMessage={handleEditMessage}
247-
onChangeSibling={setCurrNodeId}
248-
/>
249-
))}
254+
{[...messages, ...pendingMsgDisplay].map((msgDisplay) => {
255+
const actualMsgObject = msgDisplay.msg;
256+
// Check if the current message from the list is the one actively being generated/continued
257+
const isThisMessageTheActivePendingOne =
258+
pendingMsg?.id === actualMsgObject.id;
259+
260+
return (
261+
<ChatMessage
262+
key={actualMsgObject.id}
263+
// If this message is the active pending one, use the live object from pendingMsg state (which has streamed content).
264+
// Otherwise, use the version from the messages array (from storage).
265+
msg={
266+
isThisMessageTheActivePendingOne
267+
? pendingMsg
268+
: actualMsgObject
269+
}
270+
siblingLeafNodeIds={msgDisplay.siblingLeafNodeIds}
271+
siblingCurrIdx={msgDisplay.siblingCurrIdx}
272+
onRegenerateMessage={handleRegenerateMessage}
273+
onEditMessage={handleEditMessage}
274+
onChangeSibling={setCurrNodeId}
275+
// A message is pending if it's the actively streaming one OR if it came from pendingMsgDisplay (for new messages)
276+
isPending={
277+
isThisMessageTheActivePendingOne || msgDisplay.isPending
278+
}
279+
onContinueMessage={handleContinueMessage}
280+
/>
281+
);
282+
})}
250283
</div>
251284

252285
{/* chat input */}

examples/server/webui/src/components/Header.tsx

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { classNames } from '../utils/misc';
55
import daisyuiThemes from 'daisyui/theme/object';
66
import { THEMES } from '../Config';
77
import { useNavigate } from 'react-router';
8+
import toast from 'react-hot-toast';
9+
import { useModals } from './ModalProvider';
10+
import {
11+
ArrowUpTrayIcon,
12+
ArrowDownTrayIcon,
13+
PencilIcon,
14+
TrashIcon,
15+
} from '@heroicons/react/24/outline';
816

917
export default function Header() {
1018
const navigate = useNavigate();
@@ -24,9 +32,11 @@ export default function Header() {
2432
);
2533
}, [selectedTheme]);
2634

35+
const {showPrompt } = useModals();
2736
const { isGenerating, viewingChat } = useAppContext();
2837
const isCurrConvGenerating = isGenerating(viewingChat?.conv.id ?? '');
2938

39+
// remove conversation
3040
const removeConversation = () => {
3141
if (isCurrConvGenerating || !viewingChat) return;
3242
const convId = viewingChat?.conv.id;
@@ -35,6 +45,37 @@ export default function Header() {
3545
navigate('/');
3646
}
3747
};
48+
49+
// rename conversation
50+
async function renameConversation() {
51+
if (isGenerating(viewingChat?.conv.id ?? '')) {
52+
toast.error(
53+
'Cannot rename conversation while generating'
54+
);
55+
return;
56+
}
57+
const newName = await showPrompt(
58+
'Enter new name for the conversation',
59+
viewingChat?.conv.name
60+
);
61+
if (newName && newName.trim().length > 0) {
62+
StorageUtils.updateConversationName(viewingChat?.conv.id ?? '', newName);
63+
}
64+
//const importedConv = await StorageUtils.updateConversationName();
65+
//if (importedConv) {
66+
//console.log('Successfully imported:', importedConv.name);
67+
// Refresh UI or navigate to conversation
68+
//}
69+
};
70+
71+
// at the top of your file, alongside ConversationExport:
72+
async function importConversation() {
73+
const importedConv = await StorageUtils.importConversationFromFile();
74+
if (importedConv) {
75+
console.log('Successfully imported:', importedConv.name);
76+
// Refresh UI or navigate to conversation
77+
}
78+
};
3879

3980
const downloadConversation = () => {
4081
if (isCurrConvGenerating || !viewingChat) return;
@@ -99,12 +140,45 @@ export default function Header() {
99140
tabIndex={0}
100141
className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
101142
>
102-
<li onClick={downloadConversation}>
103-
<a>Download</a>
104-
</li>
105-
<li className="text-error" onClick={removeConversation}>
106-
<a>Delete</a>
107-
</li>
143+
{/* Always show Upload when viewingChat is false */}
144+
{!viewingChat && (
145+
<li onClick={importConversation}>
146+
<a>
147+
<ArrowUpTrayIcon className="w-4 h-4" />
148+
Upload
149+
</a>
150+
</li>
151+
)}
152+
153+
{/* Show all three when viewingChat is true */}
154+
{viewingChat && (
155+
<>
156+
<li onClick={importConversation}>
157+
<a>
158+
<ArrowUpTrayIcon className="w-4 h-4" />
159+
Upload
160+
</a>
161+
</li>
162+
<li onClick={renameConversation} tabIndex={0}>
163+
<a>
164+
<PencilIcon className="w-4 h-4" />
165+
Rename
166+
</a>
167+
</li>
168+
<li onClick={downloadConversation}>
169+
<a>
170+
<ArrowDownTrayIcon className="w-4 h-4" />
171+
Download
172+
</a>
173+
</li>
174+
<li className="text-error" onClick={removeConversation}>
175+
<a>
176+
<TrashIcon className="w-4 h-4" />
177+
Delete
178+
</a>
179+
</li>
180+
</>
181+
)}
108182
</ul>
109183
</div>
110184
)}

0 commit comments

Comments
 (0)