mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-25 22:21:03 +02:00
f42e29fdf1
* wip: server_tools * feat: Integrate with `/tools` endpoint * feat: Builtin + MCP + JSON Schema Tools WIP * refactor * displayName -> display_name * snake_case everywhere * rm redundant field * feat: Improvements * chore: update webui build output * refactor: Updates after server updates * chore: update webui build output * change arg to --tools all * feat: UI improvements * chore: update webui build output * add readme mention * llama-gen-docs * chore: update webui build output * chore: update webui build output * chore: update webui build output * feat: Reorganize settings sections * feat: Separate dialogs for MCP Servers Settings and Import/Export * feat: WIP * feat: WIP * feat: WIP * feat: WIP * feat: WIP * feat: WIP * WIP on allozaur/20677-webui-server-tools * feat: UI improvements * chore: Update package lock * chore: Run `npm audit fix` * feat: UI WIP * feat: UI * refactor: Desktop Icon Strip DRY * feat: Cleaner rendering and transition for ChatScreen * feat: UI improvements * feat: UI improvement * feat: Remove MCP Server "enable" switch from Tools submenu * chore: Run `npm audit fix` * feat: WIP * feat: Logic improvements * refactor: Cleanup * refactor: DRY * test: Fix Chat Sidebar UI Tests * chore: Update package lock * refactor: Cleanup * feat: Chat Message Action Card with Continue and Permission flow implementations * feat: Add agentic steering messages, draft messages and improve chat UX * fix: Search results UI * test: Fix unit test * feat: UI/UX improvements * refactor: Simplify `useToolsPanel` access in components * feat: Implement Processing Info Context API * feat: Implement 'Go back to chat' functionality for settings * feat: Enhance MCP Server management in Chat Form Attachments * style: Minor UI and branding adjustments * chore: Update webui static build output * chore: Formatting, linting & type checks * feat: Draft messages logic * feat: UI improvements * feat: Steering Messages improvements * refactor: Cleanup * refactor: Cleanup * feat: Improve UI * refactor: Settings navigation hook * refactor: DRY code * refactor: DRY ChatMessageUser UI components * refactor: Desktop Icon Strip DRY * refactor: Tools & permissions * fix: Navigation condition * refactor: Cleanup * refactor: Cleanup * refactor: Cleanup * fix: preserve reasoning_content in agentic flow --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
195 lines
5.2 KiB
Svelte
195 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
|
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
|
|
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
|
import { SvelteSet } from 'svelte/reactivity';
|
|
|
|
interface Props {
|
|
conversations: DatabaseConversation[];
|
|
messageCountMap?: Map<string, number>;
|
|
mode: 'export' | 'import';
|
|
onCancel: () => void;
|
|
onConfirm: (selectedConversations: DatabaseConversation[]) => void;
|
|
}
|
|
|
|
let { conversations, messageCountMap = new Map(), mode, onCancel, onConfirm }: Props = $props();
|
|
|
|
let searchQuery = $state('');
|
|
let selectedIds = $state.raw<SvelteSet<string>>(getInitialSelectedIds());
|
|
let lastClickedId = $state<string | null>(null);
|
|
|
|
function getInitialSelectedIds(): SvelteSet<string> {
|
|
return new SvelteSet(conversations.map((c) => c.id));
|
|
}
|
|
|
|
let filteredConversations = $derived(
|
|
conversations.filter((conv) => {
|
|
const name = conv.name || 'Untitled conversation';
|
|
return name.toLowerCase().includes(searchQuery.toLowerCase());
|
|
})
|
|
);
|
|
|
|
let allSelected = $derived(
|
|
filteredConversations.length > 0 &&
|
|
filteredConversations.every((conv) => selectedIds.has(conv.id))
|
|
);
|
|
|
|
let someSelected = $derived(
|
|
filteredConversations.some((conv) => selectedIds.has(conv.id)) && !allSelected
|
|
);
|
|
|
|
function toggleConversation(id: string, shiftKey: boolean = false) {
|
|
const newSet = new SvelteSet(selectedIds);
|
|
|
|
if (shiftKey && lastClickedId !== null) {
|
|
const lastIndex = filteredConversations.findIndex((c) => c.id === lastClickedId);
|
|
const currentIndex = filteredConversations.findIndex((c) => c.id === id);
|
|
|
|
if (lastIndex !== -1 && currentIndex !== -1) {
|
|
const start = Math.min(lastIndex, currentIndex);
|
|
const end = Math.max(lastIndex, currentIndex);
|
|
|
|
const shouldSelect = !newSet.has(id);
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
if (shouldSelect) {
|
|
newSet.add(filteredConversations[i].id);
|
|
} else {
|
|
newSet.delete(filteredConversations[i].id);
|
|
}
|
|
}
|
|
|
|
selectedIds = newSet;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (newSet.has(id)) {
|
|
newSet.delete(id);
|
|
} else {
|
|
newSet.add(id);
|
|
}
|
|
|
|
selectedIds = newSet;
|
|
lastClickedId = id;
|
|
}
|
|
|
|
function toggleAll() {
|
|
if (allSelected) {
|
|
const newSet = new SvelteSet(selectedIds);
|
|
|
|
filteredConversations.forEach((conv) => newSet.delete(conv.id));
|
|
selectedIds = newSet;
|
|
} else {
|
|
const newSet = new SvelteSet(selectedIds);
|
|
|
|
filteredConversations.forEach((conv) => newSet.add(conv.id));
|
|
selectedIds = newSet;
|
|
}
|
|
}
|
|
|
|
function handleConfirm() {
|
|
const selected = conversations.filter((conv) => selectedIds.has(conv.id));
|
|
onConfirm(selected);
|
|
}
|
|
|
|
function handleCancel() {
|
|
selectedIds = getInitialSelectedIds();
|
|
searchQuery = '';
|
|
lastClickedId = null;
|
|
|
|
onCancel();
|
|
}
|
|
|
|
export function reset() {
|
|
selectedIds = getInitialSelectedIds();
|
|
searchQuery = '';
|
|
lastClickedId = null;
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-4">
|
|
<SearchInput bind:value={searchQuery} placeholder="Search conversations..." />
|
|
|
|
<div class="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>
|
|
{selectedIds.size} of {conversations.length} selected
|
|
{#if searchQuery}
|
|
({filteredConversations.length} shown)
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="overflow-hidden rounded-md border">
|
|
<ScrollArea class="h-[400px]">
|
|
<table class="w-full">
|
|
<thead class="sticky top-0 z-10 bg-muted">
|
|
<tr class="border-b">
|
|
<th class="w-12 p-3 text-left">
|
|
<Checkbox
|
|
checked={allSelected}
|
|
indeterminate={someSelected}
|
|
onCheckedChange={toggleAll}
|
|
/>
|
|
</th>
|
|
|
|
<th class="p-3 text-left text-sm font-medium">Conversation Name</th>
|
|
|
|
<th class="w-32 p-3 text-left text-sm font-medium">Messages</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#if filteredConversations.length === 0}
|
|
<tr>
|
|
<td colspan="3" class="p-8 text-center text-sm text-muted-foreground">
|
|
{#if searchQuery}
|
|
No conversations found matching "{searchQuery}"
|
|
{:else}
|
|
No conversations available
|
|
{/if}
|
|
</td>
|
|
</tr>
|
|
{:else}
|
|
{#each filteredConversations as conv (conv.id)}
|
|
<tr
|
|
class="cursor-pointer border-b transition-colors hover:bg-muted/50"
|
|
onclick={(e) => toggleConversation(conv.id, e.shiftKey)}
|
|
>
|
|
<td class="p-3">
|
|
<Checkbox
|
|
checked={selectedIds.has(conv.id)}
|
|
onclick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
toggleConversation(conv.id, e.shiftKey);
|
|
}}
|
|
/>
|
|
</td>
|
|
|
|
<td class="p-3 text-sm">
|
|
<div class="max-w-[17rem] truncate" title={conv.name || 'Untitled conversation'}>
|
|
{conv.name || 'Untitled conversation'}
|
|
</div>
|
|
</td>
|
|
|
|
<td class="p-3 text-sm text-muted-foreground">
|
|
{messageCountMap.get(conv.id) ?? 0}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
{/if}
|
|
</tbody>
|
|
</table>
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<Button variant="outline" onclick={handleCancel}>Cancel</Button>
|
|
|
|
<Button onclick={handleConfirm} disabled={selectedIds.size === 0}>
|
|
{mode === 'export' ? 'Export' : 'Import'} ({selectedIds.size})
|
|
</Button>
|
|
</div>
|
|
</div>
|