refactor: Componentize Chat Form Prompt Picker
This commit is contained in:
parent
176abf3175
commit
fa0cad2e6e
|
|
@ -2,11 +2,13 @@
|
|||
import { mcpClient } from '$lib/clients/mcp.client';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { getFaviconUrl, debounce } from '$lib/utils';
|
||||
import { debounce } from '$lib/utils';
|
||||
import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
import ChatFormPromptPickerList from './ChatFormPromptPickerList.svelte';
|
||||
import ChatFormPromptPickerHeader from './ChatFormPromptPickerHeader.svelte';
|
||||
import ChatFormPromptPickerArgumentForm from './ChatFormPromptPickerArgumentForm.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
|
|
@ -237,6 +239,18 @@
|
|||
}, 150);
|
||||
}
|
||||
|
||||
function handleArgFocus(argName: string) {
|
||||
if ((suggestions[argName]?.length ?? 0) > 0) {
|
||||
activeAutocomplete = argName;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelArgumentForm() {
|
||||
selectedPrompt = null;
|
||||
promptArgs = {};
|
||||
promptError = null;
|
||||
}
|
||||
|
||||
export function handleKeydown(event: KeyboardEvent): boolean {
|
||||
if (!isOpen) return false;
|
||||
|
||||
|
|
@ -316,202 +330,39 @@
|
|||
<div class="overflow-hidden rounded-xl border border-border/50 bg-popover shadow-xl">
|
||||
{#if selectedPrompt}
|
||||
{@const server = serverSettingsMap.get(selectedPrompt.serverName)}
|
||||
{@const faviconUrl = server ? getFaviconUrl(server.url) : null}
|
||||
{@const serverLabel = server ? mcpStore.getServerLabel(server) : selectedPrompt.serverName}
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
class="mt-0.5 h-5 w-5 shrink-0 rounded"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ChatFormPromptPickerHeader prompt={selectedPrompt} {server} {serverLabel} />
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{server ? mcpStore.getServerLabel(server) : selectedPrompt.serverName}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">
|
||||
{selectedPrompt.title || selectedPrompt.name}
|
||||
</span>
|
||||
|
||||
{#if selectedPrompt.arguments?.length}
|
||||
<span class="rounded-full bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
||||
{selectedPrompt.arguments.length} arg{selectedPrompt.arguments.length > 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selectedPrompt.description}
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{selectedPrompt.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onsubmit={handleArgumentSubmit} class="space-y-3 pt-4">
|
||||
{#each selectedPrompt.arguments ?? [] as arg (arg.name)}
|
||||
<div class="relative grid gap-1">
|
||||
<label
|
||||
for="arg-{arg.name}"
|
||||
class="mb-1 flex items-center gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<span>
|
||||
{arg.name}
|
||||
{#if arg.required}<span class="text-destructive">*</span>{/if}
|
||||
</span>
|
||||
{#if loadingSuggestions[arg.name]}
|
||||
<span class="text-xs text-muted-foreground/50">...</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="arg-{arg.name}"
|
||||
type="text"
|
||||
value={promptArgs[arg.name] ?? ''}
|
||||
oninput={(e) => handleArgInput(arg.name, e.currentTarget.value)}
|
||||
onkeydown={(e) => handleArgKeydown(e, arg.name)}
|
||||
onblur={() => handleArgBlur(arg.name)}
|
||||
onfocus={() => {
|
||||
if ((suggestions[arg.name]?.length ?? 0) > 0) {
|
||||
activeAutocomplete = arg.name;
|
||||
}
|
||||
}}
|
||||
placeholder={arg.description || arg.name}
|
||||
required={arg.required}
|
||||
autocomplete="off"
|
||||
class="w-full rounded-lg border border-border/50 bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
|
||||
{#if activeAutocomplete === arg.name && (suggestions[arg.name]?.length ?? 0) > 0}
|
||||
<div
|
||||
class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
|
||||
transition:fly={{ y: -5, duration: 100 }}
|
||||
>
|
||||
{#each suggestions[arg.name] ?? [] as suggestion, i (suggestion)}
|
||||
<button
|
||||
type="button"
|
||||
onmousedown={() => selectSuggestion(arg.name, suggestion)}
|
||||
class="w-full px-3 py-1.5 text-left text-sm hover:bg-accent {i ===
|
||||
autocompleteIndex
|
||||
? 'bg-accent'
|
||||
: ''}"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if promptError}
|
||||
<div
|
||||
class="flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
<span class="shrink-0">⚠</span>
|
||||
<span>{promptError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
selectedPrompt = null;
|
||||
promptArgs = {};
|
||||
promptError = null;
|
||||
}}
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Use Prompt
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<ChatFormPromptPickerArgumentForm
|
||||
prompt={selectedPrompt}
|
||||
{promptArgs}
|
||||
{suggestions}
|
||||
{loadingSuggestions}
|
||||
{activeAutocomplete}
|
||||
{autocompleteIndex}
|
||||
{promptError}
|
||||
onArgInput={handleArgInput}
|
||||
onArgKeydown={handleArgKeydown}
|
||||
onArgBlur={handleArgBlur}
|
||||
onArgFocus={handleArgFocus}
|
||||
onSelectSuggestion={selectSuggestion}
|
||||
onSubmit={handleArgumentSubmit}
|
||||
onCancel={handleCancelArgumentForm}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#if showSearchInput}
|
||||
<div class="p-2 pb-0">
|
||||
<SearchInput placeholder="Search prompts..." bind:value={internalSearchQuery} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="max-h-64 overflow-y-auto p-2">
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-6 text-sm text-muted-foreground">
|
||||
Loading prompts...
|
||||
</div>
|
||||
{:else if filteredPrompts.length === 0}
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">
|
||||
{prompts.length === 0 ? 'No MCP prompts available' : 'No prompts found'}
|
||||
</div>
|
||||
{:else}
|
||||
{#each filteredPrompts as prompt, index (prompt.serverName + ':' + prompt.name)}
|
||||
{@const server = serverSettingsMap.get(prompt.serverName)}
|
||||
{@const faviconUrl = server ? getFaviconUrl(server.url) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => handlePromptClick(prompt)}
|
||||
class="flex w-full cursor-pointer items-start gap-3 rounded-lg px-3 py-2 text-left hover:bg-accent {index ===
|
||||
selectedIndex
|
||||
? 'bg-accent'
|
||||
: ''}"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
class="h-3 w-3 shrink-0 rounded-sm"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span>{server ? mcpStore.getServerLabel(server) : prompt.serverName}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{prompt.title || prompt.name}</span>
|
||||
|
||||
{#if prompt.arguments && prompt.arguments.length > 0}
|
||||
<span class="text-xs text-muted-foreground">
|
||||
({prompt.arguments.length} arg{prompt.arguments.length > 1 ? 's' : ''})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if prompt.description}
|
||||
<p class="truncate text-sm text-muted-foreground">
|
||||
{prompt.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<ChatFormPromptPickerList
|
||||
prompts={filteredPrompts}
|
||||
{isLoading}
|
||||
{selectedIndex}
|
||||
bind:searchQuery={internalSearchQuery}
|
||||
{showSearchInput}
|
||||
{serverSettingsMap}
|
||||
getServerLabel={(server) => mcpStore.getServerLabel(server)}
|
||||
onPromptClick={handlePromptClick}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts">
|
||||
import type { MCPPromptInfo } from '$lib/types';
|
||||
import ChatFormPromptPickerArgumentInput from './ChatFormPromptPickerArgumentInput.svelte';
|
||||
|
||||
interface Props {
|
||||
prompt: MCPPromptInfo;
|
||||
promptArgs: Record<string, string>;
|
||||
suggestions: Record<string, string[]>;
|
||||
loadingSuggestions: Record<string, boolean>;
|
||||
activeAutocomplete: string | null;
|
||||
autocompleteIndex: number;
|
||||
promptError: string | null;
|
||||
onArgInput: (argName: string, value: string) => void;
|
||||
onArgKeydown: (event: KeyboardEvent, argName: string) => void;
|
||||
onArgBlur: (argName: string) => void;
|
||||
onArgFocus: (argName: string) => void;
|
||||
onSelectSuggestion: (argName: string, value: string) => void;
|
||||
onSubmit: (event: SubmitEvent) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
prompt,
|
||||
promptArgs,
|
||||
suggestions,
|
||||
loadingSuggestions,
|
||||
activeAutocomplete,
|
||||
autocompleteIndex,
|
||||
promptError,
|
||||
onArgInput,
|
||||
onArgKeydown,
|
||||
onArgBlur,
|
||||
onArgFocus,
|
||||
onSelectSuggestion,
|
||||
onSubmit,
|
||||
onCancel
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit} class="space-y-3 pt-4">
|
||||
{#each prompt.arguments ?? [] as arg (arg.name)}
|
||||
<ChatFormPromptPickerArgumentInput
|
||||
argument={arg}
|
||||
value={promptArgs[arg.name] ?? ''}
|
||||
suggestions={suggestions[arg.name] ?? []}
|
||||
isLoadingSuggestions={loadingSuggestions[arg.name] ?? false}
|
||||
isAutocompleteActive={activeAutocomplete === arg.name}
|
||||
autocompleteIndex={activeAutocomplete === arg.name ? autocompleteIndex : 0}
|
||||
onInput={(value) => onArgInput(arg.name, value)}
|
||||
onKeydown={(e) => onArgKeydown(e, arg.name)}
|
||||
onBlur={() => onArgBlur(arg.name)}
|
||||
onFocus={() => onArgFocus(arg.name)}
|
||||
onSelectSuggestion={(value) => onSelectSuggestion(arg.name, value)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if promptError}
|
||||
<div
|
||||
class="flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
<span class="shrink-0">⚠</span>
|
||||
<span>{promptError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Use Prompt
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import type { MCPPromptInfo } from '$lib/types';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
type PromptArgument = NonNullable<MCPPromptInfo['arguments']>[number];
|
||||
|
||||
interface Props {
|
||||
argument: PromptArgument;
|
||||
value: string;
|
||||
suggestions?: string[];
|
||||
isLoadingSuggestions?: boolean;
|
||||
isAutocompleteActive?: boolean;
|
||||
autocompleteIndex?: number;
|
||||
onInput: (value: string) => void;
|
||||
onKeydown: (event: KeyboardEvent) => void;
|
||||
onBlur: () => void;
|
||||
onFocus: () => void;
|
||||
onSelectSuggestion: (value: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
argument,
|
||||
value = '',
|
||||
suggestions = [],
|
||||
isLoadingSuggestions = false,
|
||||
isAutocompleteActive = false,
|
||||
autocompleteIndex = 0,
|
||||
onInput,
|
||||
onKeydown,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onSelectSuggestion
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="relative grid gap-1">
|
||||
<label
|
||||
for="arg-{argument.name}"
|
||||
class="mb-1 flex items-center gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<span>
|
||||
{argument.name}
|
||||
|
||||
{#if argument.required}
|
||||
<span class="text-destructive">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if isLoadingSuggestions}
|
||||
<span class="text-xs text-muted-foreground/50">...</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="arg-{argument.name}"
|
||||
type="text"
|
||||
{value}
|
||||
oninput={(e) => onInput(e.currentTarget.value)}
|
||||
onkeydown={onKeydown}
|
||||
onblur={onBlur}
|
||||
onfocus={onFocus}
|
||||
placeholder={argument.description || argument.name}
|
||||
required={argument.required}
|
||||
autocomplete="off"
|
||||
class="w-full rounded-lg border border-border/50 bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
|
||||
{#if isAutocompleteActive && suggestions.length > 0}
|
||||
<div
|
||||
class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
|
||||
transition:fly={{ y: -5, duration: 100 }}
|
||||
>
|
||||
{#each suggestions as suggestion, i (suggestion)}
|
||||
<button
|
||||
type="button"
|
||||
onmousedown={() => onSelectSuggestion(suggestion)}
|
||||
class="w-full px-3 py-1.5 text-left text-sm hover:bg-accent {i === autocompleteIndex
|
||||
? 'bg-accent'
|
||||
: ''}"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import type { MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
prompt: MCPPromptInfo;
|
||||
server: MCPServerSettingsEntry | undefined;
|
||||
serverLabel: string;
|
||||
}
|
||||
|
||||
let { prompt, server, serverLabel }: Props = $props();
|
||||
|
||||
let faviconUrl = $derived(server ? getFaviconUrl(server.url) : null);
|
||||
</script>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
class="mt-0.5 h-5 w-5 shrink-0 rounded"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{serverLabel}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">
|
||||
{prompt.title || prompt.name}
|
||||
</span>
|
||||
|
||||
{#if prompt.arguments?.length}
|
||||
<span class="rounded-full bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
||||
{prompt.arguments.length} arg{prompt.arguments.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if prompt.description}
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{prompt.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="ts">
|
||||
import type { MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
import ChatFormPromptPickerListItem from './ChatFormPromptPickerListItem.svelte';
|
||||
import ChatFormPromptPickerListItemSkeleton from './ChatFormPromptPickerListItemSkeleton.svelte';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
prompts: MCPPromptInfo[];
|
||||
isLoading: boolean;
|
||||
selectedIndex: number;
|
||||
searchQuery: string;
|
||||
showSearchInput: boolean;
|
||||
serverSettingsMap: SvelteMap<string, MCPServerSettingsEntry>;
|
||||
getServerLabel: (server: MCPServerSettingsEntry) => string;
|
||||
onPromptClick: (prompt: MCPPromptInfo) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
prompts,
|
||||
isLoading,
|
||||
selectedIndex,
|
||||
searchQuery = $bindable(),
|
||||
showSearchInput,
|
||||
serverSettingsMap,
|
||||
getServerLabel,
|
||||
onPromptClick
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if showSearchInput}
|
||||
<div class="p-2 pb-0">
|
||||
<SearchInput placeholder="Search prompts..." bind:value={searchQuery} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="max-h-64 overflow-y-auto p-2">
|
||||
{#if isLoading}
|
||||
<ChatFormPromptPickerListItemSkeleton />
|
||||
{:else if prompts.length === 0}
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">
|
||||
{prompts.length === 0 ? 'No MCP prompts available' : 'No prompts found'}
|
||||
</div>
|
||||
{:else}
|
||||
{#each prompts as prompt, index (prompt.serverName + ':' + prompt.name)}
|
||||
{@const server = serverSettingsMap.get(prompt.serverName)}
|
||||
{@const serverLabel = server ? getServerLabel(server) : prompt.serverName}
|
||||
|
||||
<ChatFormPromptPickerListItem
|
||||
{prompt}
|
||||
{server}
|
||||
{serverLabel}
|
||||
isSelected={index === selectedIndex}
|
||||
onClick={() => onPromptClick(prompt)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import type { MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
prompt: MCPPromptInfo;
|
||||
server: MCPServerSettingsEntry | undefined;
|
||||
serverLabel: string;
|
||||
isSelected?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
let { prompt, server, serverLabel, isSelected = false, onClick }: Props = $props();
|
||||
|
||||
let faviconUrl = $derived(server ? getFaviconUrl(server.url) : null);
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClick}
|
||||
class="flex w-full cursor-pointer items-start gap-3 rounded-lg px-3 py-2 text-left hover:bg-accent {isSelected
|
||||
? 'bg-accent'
|
||||
: ''}"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
class="h-3 w-3 shrink-0 rounded-sm"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span>{serverLabel}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{prompt.title || prompt.name}</span>
|
||||
|
||||
{#if prompt.arguments && prompt.arguments.length > 0}
|
||||
<span class="text-xs text-muted-foreground">
|
||||
({prompt.arguments.length} arg{prompt.arguments.length > 1 ? 's' : ''})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if prompt.description}
|
||||
<p class="truncate text-sm text-muted-foreground">
|
||||
{prompt.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div class="flex w-full items-start gap-3 rounded-lg px-3 py-2">
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<!-- Server label skeleton -->
|
||||
<div class="mb-2 flex items-center gap-1.5">
|
||||
<div class="h-3 w-3 shrink-0 animate-pulse rounded-sm bg-muted"></div>
|
||||
<div class="h-3 w-24 animate-pulse rounded bg-muted"></div>
|
||||
</div>
|
||||
|
||||
<!-- Title skeleton -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-4 w-32 animate-pulse rounded bg-muted"></div>
|
||||
<div class="h-4 w-12 animate-pulse rounded-full bg-muted"></div>
|
||||
</div>
|
||||
|
||||
<!-- Description skeleton -->
|
||||
<div class="h-3 w-full animate-pulse rounded bg-muted"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -14,7 +14,13 @@ export { default as ChatFormActions } from './chat/ChatForm/ChatFormActions/Chat
|
|||
export { default as ChatFormActionSubmit } from './chat/ChatForm/ChatFormActions/ChatFormActionSubmit.svelte';
|
||||
export { default as ChatFormFileInputInvisible } from './chat/ChatForm/ChatFormFileInputInvisible.svelte';
|
||||
export { default as ChatFormHelperText } from './chat/ChatForm/ChatFormHelperText.svelte';
|
||||
export { default as ChatFormPromptPicker } from './chat/ChatForm/ChatFormPromptPicker.svelte';
|
||||
export { default as ChatFormPromptPicker } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPicker.svelte';
|
||||
export { default as ChatFormPromptPickerArgumentForm } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPickerArgumentForm.svelte';
|
||||
export { default as ChatFormPromptPickerArgumentInput } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPickerArgumentInput.svelte';
|
||||
export { default as ChatFormPromptPickerHeader } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPickerHeader.svelte';
|
||||
export { default as ChatFormPromptPickerList } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPickerList.svelte';
|
||||
export { default as ChatFormPromptPickerListItem } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPickerListItem.svelte';
|
||||
export { default as ChatFormPromptPickerListItemSkeleton } from './chat/ChatForm/ChatFormPromptPicker/ChatFormPromptPickerListItemSkeleton.svelte';
|
||||
export { default as ChatFormTextarea } from './chat/ChatForm/ChatFormTextarea.svelte';
|
||||
|
||||
export { default as ChatMessage } from './chat/ChatMessages/ChatMessage.svelte';
|
||||
|
|
|
|||
Loading…
Reference in New Issue