mirror of
https://github.com/ikawrakow/ik_llama.cpp.git
synced 2026-02-04 13:30:47 +00:00
webui: add system message in export conversation, support upload conversation with system message Webui: show upload only when in new conversation Webui: Add model name webui: increase height of chat message window when clicking editing Webui: autoclose settings dialog dropdown and maximze screen width when zoom in webui: fix date issues and add more dates webui: change error to toast.error. server: add n_past and slot_id in props_simple webui: add cache tokens, context and prompt speed in chat webui: modernize ui webui: change welcome message webui: change speed display webui: change run python icon webui: add config to use server defaults for sampler webui: put speed on left and context on right webui: recognize AsciiDoc files as valid text files (#16850) * webui: recognize AsciiDoc files as valid text files * webui: add an updated static webui build * webui: add the updated dependency list * webui: re-add an updated static webui build Add a setting to display message generation statistics (#16901) * feat: Add setting to display message generation statistics * chore: build static webui output webui: add HTML/JS preview support to MarkdownContent with sandboxed iframe (#16757) * webui: add HTML/JS preview support to MarkdownContent with sandboxed iframe dialog Extended MarkdownContent to flag previewable code languages, add a preview button alongside copy controls, manage preview dialog state, and share styling for the new button group Introduced CodePreviewDialog.svelte, a sandboxed iframe modal for rendering HTML/JS previews with consistent dialog controls * webui: fullscreen HTML preview dialog using bits-ui * Update tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * Update tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * webui: pedantic style tweak for CodePreviewDialog close button * webui: remove overengineered preview language logic * chore: update webui static build --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> webui: auto-refresh /props on inference start to resync model metadata (#16784) * webui: auto-refresh /props on inference start to resync model metadata - Add no-cache headers to /props and /slots - Throttle slot checks to 30s - Prevent concurrent fetches with promise guard - Trigger refresh from chat streaming for legacy and ModelSelector - Show dynamic serverWarning when using cached data * fix: restore proper legacy behavior in webui by using unified /props refresh Updated assistant message bubbles to show each message's stored model when available, falling back to the current server model only when the per-message value is missing When the model selector is disabled, now fetches /props and prioritizes that model name over chunk metadata, then persists it with the streamed message so legacy mode properly reflects the backend configuration * fix: detect first valid SSE chunk and refresh server props once * fix: removed the slots availability throttle constant and state * webui: purge ai-generated cruft * chore: update webui static build feat(webui): improve LaTeX rendering with currency detection (#16508) * webui : Revised LaTeX formula recognition * webui : Further examples containg amounts * webui : vitest for maskInlineLaTeX * webui: Moved preprocessLaTeX to lib/utils * webui: LaTeX in table-cells * chore: update webui build output (use theirs) * webui: backslash in LaTeX-preprocessing * chore: update webui build output * webui: look-behind backslash-check * chore: update webui build output * Apply suggestions from code review Code maintenance (variable names, code formatting, string handling) Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * webui: Moved constants to lib/constants. * webui: package woff2 inside base64 data * webui: LaTeX-line-break in display formula * chore: update webui build output * webui: Bugfix (font embedding) * webui: Bugfix (font embedding) * webui: vite embeds assets * webui: don't suppress 404 (fonts) * refactor: KaTeX integration with SCSS Moves KaTeX styling to SCSS for better customization and font embedding. This change includes: - Adding `sass` as a dev dependency. - Introducing a custom SCSS file to override KaTeX variables and disable TTF/WOFF fonts, relying solely on WOFF2 for embedding. - Adjusting the Vite configuration to resolve `katex-fonts` alias and inject SCSS variables. * fix: LaTeX processing within blockquotes * webui: update webui build output --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> server : add props.model_alias (#16943) * server : add props.model_alias webui: fix keyboard shortcuts for new chat & edit chat title (#17007) Better UX for handling multiple attachments in WebUI (#17246) webui: add OAI-Compat Harmony tool-call streaming visualization and persistence in chat UI (#16618) * webui: add OAI-Compat Harmony tool-call live streaming visualization and persistence in chat UI - Purely visual and diagnostic change, no effect on model context, prompt construction, or inference behavior - Captured assistant tool call payloads during streaming and non-streaming completions, and persisted them in chat state and storage for downstream use - Exposed parsed tool call labels beneath the assistant's model info line with graceful fallback when parsing fails - Added tool call badges beneath assistant responses that expose JSON tooltips and copy their payloads when clicked, matching the existing model badge styling - Added a user-facing setting to toggle tool call visibility to the Developer settings section directly under the model selector option * webui: remove scroll listener causing unnecessary layout updates (model selector) * Update tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * Update tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * chore: npm run format & update webui build output * chore: update webui build output --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> webui: Fix clickability around chat processing statistics UI (#17278) * fix: Better pointer events handling in chat processing info elements * chore: update webui build output Fix merge error webui: Add a "Continue" Action for Assistant Message (#16971) * feat: Add "Continue" action for assistant messages * feat: Continuation logic & prompt improvements * chore: update webui build output * feat: Improve logic for continuing the assistant message * chore: update webui build output * chore: Linting * chore: update webui build output * fix: Remove synthetic prompt logic, use the prefill feature by sending the conversation payload ending with assistant message * chore: update webui build output * feat: Enable "Continue" button based on config & non-reasoning model type * chore: update webui build output * chore: Update packages with `npm audit fix` * fix: Remove redundant error * chore: update webui build output * chore: Update `.gitignore` * fix: Add missing change * feat: Add auto-resizing for Edit Assistant/User Message textareas * chore: update webui build output Improved file naming & structure for UI components (#17405) * refactor: Component iles naming & structure * chore: update webui build output * refactor: Dialog titles + components namig * chore: update webui build output * refactor: Imports * chore: update webui build output webui: hide border of button webui: update webui: update webui: update add vision webui: minor settings reorganization and add disable autoscroll option (#17452) * webui: added a dedicated 'Display' settings section that groups visualization options * webui: added a Display setting to toggle automatic chat scrolling * chore: update webui build output Co-authored-by: firecoperana <firecoperana>
417 lines
13 KiB
TypeScript
417 lines
13 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { classNames } from '../utils/misc';
|
|
import { Conversation } from '../utils/types';
|
|
import StorageUtils from '../utils/storage';
|
|
import { useNavigate, useParams } from 'react-router';
|
|
import {
|
|
ArrowUpTrayIcon,
|
|
ArrowDownTrayIcon,
|
|
EllipsisVerticalIcon,
|
|
PencilIcon,
|
|
PencilSquareIcon,
|
|
TrashIcon,
|
|
XMarkIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { BtnWithTooltips } from '../utils/common';
|
|
import { useAppContext } from '../utils/app.context';
|
|
import toast from 'react-hot-toast';
|
|
import { useModals } from './ModalProvider';
|
|
import {DateTime} from 'luxon'
|
|
|
|
// at the top of your file, alongside ConversationExport:
|
|
async function importConversation() {
|
|
const importedConv = await StorageUtils.importConversationFromFile();
|
|
if (importedConv) {
|
|
console.log('Successfully imported:', importedConv.name);
|
|
// Refresh UI or navigate to conversation
|
|
}
|
|
};
|
|
export default function Sidebar() {
|
|
const params = useParams();
|
|
const navigate = useNavigate();
|
|
|
|
const { isGenerating, viewingChat } = useAppContext();
|
|
|
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
const [currConv, setCurrConv] = useState<Conversation | null>(null);
|
|
|
|
useEffect(() => {
|
|
StorageUtils.getOneConversation(params.convId ?? '').then(setCurrConv);
|
|
}, [params.convId]);
|
|
|
|
useEffect(() => {
|
|
const handleConversationChange = async () => {
|
|
setConversations(await StorageUtils.getAllConversations());
|
|
};
|
|
StorageUtils.onConversationChanged(handleConversationChange);
|
|
handleConversationChange();
|
|
return () => {
|
|
StorageUtils.offConversationChanged(handleConversationChange);
|
|
};
|
|
}, []);
|
|
const { showConfirm, showPrompt } = useModals();
|
|
|
|
const groupedConv = useMemo(
|
|
() => groupConversationsByDate(conversations),
|
|
[conversations]
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
id="toggle-drawer"
|
|
type="checkbox"
|
|
className="drawer-toggle"
|
|
aria-label="Toggle sidebar"
|
|
defaultChecked
|
|
/>
|
|
|
|
<div
|
|
className="drawer-side h-screen lg:h-screen z-50 lg:max-w-64"
|
|
role="complementary"
|
|
aria-label="Sidebar"
|
|
tabIndex={0}
|
|
>
|
|
<label
|
|
htmlFor="toggle-drawer"
|
|
aria-label="Close sidebar"
|
|
className="drawer-overlay"
|
|
></label>
|
|
|
|
<a
|
|
href="#main-scroll"
|
|
className="absolute -left-80 top-0 w-1 h-1 overflow-hidden"
|
|
>
|
|
Skip to main content
|
|
</a>
|
|
|
|
<div className="flex flex-col bg-base-200 min-h-full max-w-64 py-4 px-4">
|
|
<div className="flex flex-row items-center justify-between mb-4 mt-4">
|
|
<h2 className="font-bold ml-4" role="heading">
|
|
Conversations
|
|
</h2>
|
|
|
|
{/* close sidebar button */}
|
|
<label
|
|
htmlFor="toggle-drawer"
|
|
className="btn btn-ghost lg:hidden"
|
|
aria-label="Close sidebar"
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<XMarkIcon className="w-5 h-5" />
|
|
</label>
|
|
</div>
|
|
|
|
{/* new conversation button */}
|
|
<button
|
|
className={classNames({
|
|
'btn btn-ghost justify-start px-2': true,
|
|
'btn-soft': !currConv,
|
|
})}
|
|
onClick={() => navigate('/')}
|
|
aria-label="New conversation"
|
|
>
|
|
<PencilSquareIcon className="w-5 h-5" />
|
|
New Conversations
|
|
</button>
|
|
|
|
{/* list of conversations */}
|
|
{groupedConv.map((group, i) => (
|
|
<div key={i} role="group">
|
|
{/* group name (by date) */}
|
|
{group.title ? (
|
|
// we use btn class here to make sure that the padding/margin are aligned with the other items
|
|
<b
|
|
className="btn btn-ghost btn-xs bg-none btn-disabled block text-xs text-base-content text-start px-2 mb-0 mt-6 font-bold"
|
|
role="note"
|
|
aria-description={group.title}
|
|
tabIndex={0}
|
|
>
|
|
{group.title}
|
|
</b>
|
|
) : (
|
|
<div className="h-2" />
|
|
)}
|
|
|
|
{group.conversations.map((conv) => (
|
|
<ConversationItem
|
|
key={conv.id}
|
|
conv={conv}
|
|
isCurrConv={currConv?.id === conv.id}
|
|
onSelect={() => {
|
|
navigate(`/chat/${conv.id}`);
|
|
}}
|
|
onDelete={async () => {
|
|
if (isGenerating(conv.id)) {
|
|
toast.error(
|
|
'Cannot delete conversation while generating'
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
await showConfirm(
|
|
'Are you sure to delete this conversation?'
|
|
)
|
|
) {
|
|
toast.success('Conversation deleted');
|
|
StorageUtils.remove(conv.id);
|
|
navigate('/');
|
|
}
|
|
}}
|
|
|
|
onDownload={() => {
|
|
if (isGenerating(conv.id)) {
|
|
toast.error(
|
|
'Cannot download conversation while generating'
|
|
);
|
|
return;
|
|
}
|
|
// Get the current system message from config
|
|
const systemMessage = StorageUtils.getConfig().systemMessage;
|
|
|
|
// Clone the viewingChat object to avoid modifying the original
|
|
const exportData = {
|
|
conv: { ...viewingChat?.conv },
|
|
messages: viewingChat?.messages.map(msg => ({ ...msg }))
|
|
};
|
|
|
|
// Find the root message and update its content
|
|
const rootMessage = exportData.messages?.find(m => m.type === 'root');
|
|
if (rootMessage) {
|
|
rootMessage.content = systemMessage;
|
|
}
|
|
|
|
|
|
const conversationJson = JSON.stringify(exportData, null, 2);
|
|
// const conversationJson = JSON.stringify(conv, null, 2);
|
|
const blob = new Blob([conversationJson], {
|
|
type: 'application/json',
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `conversation_${conv.id}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}}
|
|
onRename={async () => {
|
|
if (isGenerating(conv.id)) {
|
|
toast.error(
|
|
'Cannot rename conversation while generating'
|
|
);
|
|
return;
|
|
}
|
|
const newName = await showPrompt(
|
|
'Enter new name for the conversation',
|
|
conv.name
|
|
);
|
|
if (newName && newName.trim().length > 0) {
|
|
StorageUtils.updateConversationName(conv.id, newName);
|
|
}
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
))}
|
|
<div className="text-center text-xs opacity-40 mt-auto mx-4 pt-8">
|
|
Conversations are saved to browser's IndexedDB
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ConversationItem({
|
|
conv,
|
|
isCurrConv,
|
|
onSelect,
|
|
onDelete,
|
|
onDownload,
|
|
onRename,
|
|
}: {
|
|
conv: Conversation;
|
|
isCurrConv: boolean;
|
|
onSelect: () => void;
|
|
onDelete: () => void;
|
|
onDownload: () => void;
|
|
onRename: () => void;
|
|
}) {
|
|
return (
|
|
<div
|
|
role="menuitem"
|
|
tabIndex={0}
|
|
aria-label={conv.name}
|
|
className={classNames({
|
|
'group flex flex-row btn btn-ghost justify-start items-center font-normal px-2 h-9':
|
|
true,
|
|
'btn-soft': isCurrConv,
|
|
})}
|
|
onClick={onSelect}
|
|
>
|
|
<button
|
|
key={conv.id}
|
|
className="w-full overflow-hidden truncate text-start"
|
|
dir="auto"
|
|
>
|
|
{conv.name}
|
|
</button>
|
|
<div className="dropdown dropdown-end h-5">
|
|
<BtnWithTooltips
|
|
// on mobile, we always show the ellipsis icon
|
|
// on desktop, we only show it when the user hovers over the conversation item
|
|
// we use opacity instead of hidden to avoid layout shift
|
|
className="cursor-pointer opacity-100 xl:opacity-0 group-hover:opacity-100"
|
|
onClick={() => {}}
|
|
tooltipsContent="More"
|
|
>
|
|
<EllipsisVerticalIcon className="w-5 h-5" />
|
|
</BtnWithTooltips>
|
|
{/* dropdown menu */}
|
|
<ul
|
|
aria-label="More options"
|
|
tabIndex={0}
|
|
className="dropdown-content menu bg-base-100 rounded-box z-[1] p-2 shadow"
|
|
>
|
|
{/* Always show Upload when viewingChat is false */}
|
|
|
|
<li onClick={importConversation}>
|
|
<a>
|
|
<ArrowUpTrayIcon className="w-4 h-4" />
|
|
Upload
|
|
</a>
|
|
</li>
|
|
<li onClick={onRename} tabIndex={0}>
|
|
<a>
|
|
<PencilIcon className="w-4 h-4" />
|
|
Rename
|
|
</a>
|
|
</li>
|
|
<li onClick={onDownload} tabIndex={0}>
|
|
<a>
|
|
<ArrowDownTrayIcon className="w-4 h-4" />
|
|
Download
|
|
</a>
|
|
</li>
|
|
<li className="text-error" onClick={onDelete} tabIndex={0}>
|
|
<a>
|
|
<TrashIcon className="w-4 h-4" />
|
|
Delete
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// WARN: vibe code below
|
|
|
|
export interface GroupedConversations {
|
|
title?: string;
|
|
conversations: Conversation[];
|
|
}
|
|
|
|
// TODO @ngxson : add test for this function
|
|
// Group conversations by date
|
|
// - Yesterday
|
|
// - "Previous 7 Days"
|
|
// - "Previous 30 Days"
|
|
// - "Month Year" (e.g., "April 2023")
|
|
export function groupConversationsByDate(
|
|
conversations: Conversation[]
|
|
): GroupedConversations[] {
|
|
|
|
const today=DateTime.now().startOf('day');
|
|
const yesterday = today.minus({ days: 1 });
|
|
|
|
const yesterday2 = today.minus({ days: 2});
|
|
|
|
const sevenDaysAgo = today.minus({ days: 7 });
|
|
|
|
const thirtyDaysAgo = today.minus({ days: 30 });
|
|
const groups: { [key: string]: Conversation[] } = {
|
|
Today: [],
|
|
Yesterday: [],
|
|
'Previous 2 Days': [],
|
|
'Previous 7 Days': [],
|
|
'Previous 30 Days': [],
|
|
};
|
|
const monthlyGroups: { [key: string]: Conversation[] } = {}; // Key format: "Month Year" e.g., "April 2023"
|
|
|
|
// Sort conversations by lastModified date in descending order (newest first)
|
|
// This helps when adding to groups, but the final output order of groups is fixed.
|
|
const sortedConversations = [...conversations].sort(
|
|
(a, b) => b.lastModified - a.lastModified
|
|
);
|
|
|
|
for (const conv of sortedConversations) {
|
|
const convDate=DateTime.fromMillis(conv.lastModified).setZone('America/Chicago');
|
|
if (convDate >= today) {
|
|
groups['Today'].push(conv);
|
|
} else if (convDate >= yesterday) {
|
|
groups['Yesterday'].push(conv);
|
|
} else if (convDate >= yesterday2) {
|
|
groups['Previous 2 Days'].push(conv);
|
|
} else if (convDate >= sevenDaysAgo) {
|
|
groups['Previous 7 Days'].push(conv);
|
|
} else if (convDate >= thirtyDaysAgo) {
|
|
groups['Previous 30 Days'].push(conv);
|
|
} else {
|
|
const monthName = convDate.monthLong;
|
|
const year = convDate.year;
|
|
const monthYearKey = `${monthName} ${year}`;
|
|
if (!monthlyGroups[monthYearKey]) {
|
|
monthlyGroups[monthYearKey] = [];
|
|
}
|
|
monthlyGroups[monthYearKey].push(conv);
|
|
}
|
|
}
|
|
|
|
const result: GroupedConversations[] = [];
|
|
|
|
if (groups['Today'].length > 0) {
|
|
result.push({
|
|
title: undefined, // no title for Today
|
|
conversations: groups['Today'],
|
|
});
|
|
}
|
|
const timeRanges = [
|
|
{ key: 'Yesterday', display: 'Yesterday'},
|
|
{ key: 'Previous 2 Days', display: 'Previous 2 Days'},
|
|
{ key: 'Previous 7 Days', display: 'Previous 7 Days' },
|
|
{ key: 'Previous 30 Days', display: 'Previous 30 Days' },
|
|
|
|
// Add more ranges here if needed, e.g., 'Previous 90 Days'
|
|
];
|
|
|
|
for (const range of timeRanges) {
|
|
if (groups[range.key]?.length > 0) {
|
|
result.push({
|
|
title: range.display,
|
|
conversations: groups[range.key]
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort monthly groups by date (most recent month first)
|
|
const sortedMonthKeys = Object.keys(monthlyGroups).sort((a, b) => {
|
|
const dateA = new Date(a); // "Month Year" can be parsed by Date constructor
|
|
const dateB = new Date(b);
|
|
return dateB.getTime() - dateA.getTime();
|
|
});
|
|
|
|
for (const monthKey of sortedMonthKeys) {
|
|
if (monthlyGroups[monthKey].length > 0) {
|
|
result.push({ title: monthKey, conversations: monthlyGroups[monthKey] });
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|