refactor: Cleanup
This commit is contained in:
parent
c7b7fc6c15
commit
2aa704b821
|
|
@ -16,7 +16,7 @@
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 bg-white/20 p-0 hover:bg-white/30 {className}"
|
||||
onclick={(e) => {
|
||||
onclick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onRemove?.(id);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
/** Styled button for action triggers with icon support. */
|
||||
export { default as ActionButton } from './ActionButton.svelte';
|
||||
|
||||
/** Code block actions component (copy, preview). */
|
||||
export { default as CodeBlockActions } from '../actions/CodeBlockActions.svelte';
|
||||
|
||||
/** Copy-to-clipboard button with success feedback. */
|
||||
export { default as CopyToClipboardIcon } from './CopyToClipboardIcon.svelte';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { FileText, X, Loader2, AlertCircle, Database, Image, Code, File } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { FileText, Loader2, AlertCircle, Database, Image, Code, File } from '@lucide/svelte';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
import type { MCPResourceAttachment, MCPResourceInfo } from '$lib/types';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { RemoveButton } from '../../actions';
|
||||
|
||||
interface Props {
|
||||
attachment: MCPResourceAttachment;
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'flex flex-shrink-0 items-center gap-2 rounded-md border px-2 py-1 text-sm transition-colors',
|
||||
'flex flex-shrink-0 items-center gap-2 rounded-md border p-0.5 pl-2 text-sm transition-colors',
|
||||
getStatusClass(attachment),
|
||||
onClick && 'cursor-pointer hover:bg-muted/50',
|
||||
className
|
||||
|
|
@ -87,18 +87,7 @@
|
|||
</span>
|
||||
|
||||
{#if onRemove}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-5 w-5 p-0 hover:bg-destructive/20"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove(attachment.id);
|
||||
}}
|
||||
title="Remove attachment"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</Button>
|
||||
<RemoveButton class="bg-transparent " id={attachment.id} {onRemove} />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { ChatMessageActions, ChatMessageMcpPromptContent } from '$lib/components/app';
|
||||
import {
|
||||
ChatMessageActions,
|
||||
ChatMessageEditForm,
|
||||
ChatMessageMcpPromptContent
|
||||
} from '$lib/components/app';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { MessageRole, McpPromptVariant } from '$lib/enums';
|
||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||
import ChatMessageEditForm from './ChatMessageEditForm.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
|
|
|
|||
|
|
@ -45,18 +45,6 @@
|
|||
return map;
|
||||
});
|
||||
|
||||
function getServerFavicon(): string | null {
|
||||
const server = serverSettingsMap.get(prompt.serverName);
|
||||
return server ? getFaviconUrl(server.url) : null;
|
||||
}
|
||||
|
||||
function getServerDisplayName(): string {
|
||||
const server = serverSettingsMap.get(prompt.serverName);
|
||||
if (!server) return prompt.serverName;
|
||||
|
||||
return mcpStore.getServerLabel(server);
|
||||
}
|
||||
|
||||
let contentParts = $derived.by((): ContentPart[] => {
|
||||
if (!prompt.content || !hasArguments) {
|
||||
return [{ text: prompt.content || '', argKey: null }];
|
||||
|
|
@ -107,6 +95,18 @@
|
|||
let maxHeightStyle = $derived(
|
||||
isAttachment ? 'max-height: 10rem;' : 'max-height: var(--max-message-height);'
|
||||
);
|
||||
|
||||
function getServerFavicon(): string | null {
|
||||
const server = serverSettingsMap.get(prompt.serverName);
|
||||
return server ? getFaviconUrl(server.url) : null;
|
||||
}
|
||||
|
||||
function getServerDisplayName(): string {
|
||||
const server = serverSettingsMap.get(prompt.serverName);
|
||||
if (!server) return prompt.serverName;
|
||||
|
||||
return mcpStore.getServerLabel(server);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 {className}">
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
AlertTriangle,
|
||||
Code,
|
||||
Monitor,
|
||||
Sun,
|
||||
Moon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Database
|
||||
|
|
@ -23,6 +21,7 @@
|
|||
import { ColorMode } from '$lib/enums/ui';
|
||||
import type { Component } from 'svelte';
|
||||
import { NUMERIC_FIELDS, POSITIVE_INTEGER_FIELDS } from '$lib/constants/settings-fields';
|
||||
import { SETTINGS_COLOR_MODES_CONFIG } from '$lib/constants/settings-config';
|
||||
|
||||
interface Props {
|
||||
onSave?: () => void;
|
||||
|
|
@ -43,11 +42,7 @@
|
|||
key: 'theme',
|
||||
label: 'Theme',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: ColorMode.SYSTEM, label: 'System', icon: Monitor },
|
||||
{ value: ColorMode.LIGHT, label: 'Light', icon: Sun },
|
||||
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
|
||||
]
|
||||
options: SETTINGS_COLOR_MODES_CONFIG
|
||||
},
|
||||
{ key: 'apiKey', label: 'API Key', type: 'input' },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* CollapsibleInfoCard - Reusable collapsible card component
|
||||
*
|
||||
* Used for displaying thinking content, tool calls, and other collapsible information
|
||||
* with a consistent UI pattern.
|
||||
*
|
||||
* Features auto-scroll during streaming: scrolls to bottom automatically,
|
||||
* stops when user scrolls up, resumes when user scrolls back to bottom.
|
||||
*/
|
||||
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@
|
|||
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
||||
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { DialogCodePreview } from '$lib/components/app/dialogs';
|
||||
import CodeBlockActions from './CodeBlockActions.svelte';
|
||||
import { CodeBlockActions, DialogCodePreview } from '$lib/components/app';
|
||||
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
|
||||
import type { DatabaseMessage } from '$lib/types/database';
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,3 @@ export { default as SyntaxHighlightedCode } from './SyntaxHighlightedCode.svelte
|
|||
* ```
|
||||
*/
|
||||
export { default as CollapsibleContentBlock } from './CollapsibleContentBlock.svelte';
|
||||
|
||||
/** Code block actions component (copy, preview). */
|
||||
export { default as CodeBlockActions } from './CodeBlockActions.svelte';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { FolderOpen, Plus, Loader2 } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
|
|
@ -29,7 +30,8 @@
|
|||
loadResources();
|
||||
|
||||
if (preSelectedUri) {
|
||||
selectedResources = new SvelteSet([preSelectedUri]);
|
||||
selectedResources.clear();
|
||||
selectedResources.add(preSelectedUri);
|
||||
lastSelectedUri = preSelectedUri;
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +49,7 @@
|
|||
open = newOpen;
|
||||
onOpenChange?.(newOpen);
|
||||
if (!newOpen) {
|
||||
selectedResources = new SvelteSet();
|
||||
selectedResources.clear();
|
||||
lastSelectedUri = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,28 +63,24 @@
|
|||
if (lastIndex !== -1 && currentIndex !== -1) {
|
||||
const start = Math.min(lastIndex, currentIndex);
|
||||
const end = Math.max(lastIndex, currentIndex);
|
||||
const newSelection = new SvelteSet(selectedResources);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
newSelection.add(allResources[i].uri);
|
||||
selectedResources.add(allResources[i].uri);
|
||||
}
|
||||
|
||||
selectedResources = newSelection;
|
||||
}
|
||||
} else {
|
||||
selectedResources = new SvelteSet([resource.uri]);
|
||||
selectedResources.clear();
|
||||
selectedResources.add(resource.uri);
|
||||
lastSelectedUri = resource.uri;
|
||||
}
|
||||
}
|
||||
|
||||
function handleResourceToggle(resource: MCPResourceInfo, checked: boolean) {
|
||||
const newSelection = new SvelteSet(selectedResources);
|
||||
if (checked) {
|
||||
newSelection.add(resource.uri);
|
||||
selectedResources.add(resource.uri);
|
||||
} else {
|
||||
newSelection.delete(resource.uri);
|
||||
selectedResources.delete(resource.uri);
|
||||
}
|
||||
selectedResources = newSelection;
|
||||
lastSelectedUri = resource.uri;
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +110,13 @@
|
|||
onAttach?.(resource);
|
||||
}
|
||||
|
||||
const count = resourcesToAttach.length;
|
||||
toast.success(
|
||||
count === 1
|
||||
? `Resource attached: ${resourcesToAttach[0].name}`
|
||||
: `${count} resources attached`
|
||||
);
|
||||
|
||||
handleOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to attach resources:', error);
|
||||
|
|
@ -125,6 +130,7 @@
|
|||
try {
|
||||
await mcpStore.attachResource(resource.uri);
|
||||
onAttach?.(resource);
|
||||
toast.success(`Resource attached: ${resource.name}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to attach resource:', error);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@
|
|||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpResources, mcpResourcesLoading } from '$lib/stores/mcp-resources.svelte';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
import { TruncatedText } from '$lib/components/app';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import type { MCPResource, MCPResourceInfo } from '$lib/types';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
|
|
@ -43,15 +41,15 @@
|
|||
|
||||
let expandedServers = new SvelteSet<string>();
|
||||
let expandedFolders = new SvelteSet<string>();
|
||||
let hasAutoExpanded = $state(false);
|
||||
let lastExpandedUri = $state<string | undefined>(undefined);
|
||||
|
||||
const resources = $derived(mcpResources());
|
||||
const isLoading = $derived(mcpResourcesLoading());
|
||||
|
||||
$effect(() => {
|
||||
if (expandToUri && resources.size > 0 && !hasAutoExpanded) {
|
||||
if (expandToUri && resources.size > 0 && expandToUri !== lastExpandedUri) {
|
||||
autoExpandToResource(expandToUri);
|
||||
hasAutoExpanded = true;
|
||||
lastExpandedUri = expandToUri;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -59,20 +57,16 @@
|
|||
for (const [serverName, serverRes] of resources.entries()) {
|
||||
const resource = serverRes.resources.find((r) => r.uri === uri);
|
||||
if (resource) {
|
||||
const newExpandedServers = new SvelteSet(expandedServers);
|
||||
newExpandedServers.add(serverName);
|
||||
expandedServers = newExpandedServers;
|
||||
expandedServers.add(serverName);
|
||||
|
||||
const pathParts = parseResourcePath(uri);
|
||||
if (pathParts.length > 1) {
|
||||
const newExpandedFolders = new SvelteSet(expandedFolders);
|
||||
let currentPath = '';
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
currentPath = `${currentPath}/${pathParts[i]}`;
|
||||
const folderId = `${serverName}:${currentPath}`;
|
||||
newExpandedFolders.add(folderId);
|
||||
expandedFolders.add(folderId);
|
||||
}
|
||||
expandedFolders = newExpandedFolders;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -80,23 +74,19 @@
|
|||
}
|
||||
|
||||
function toggleServer(serverName: string) {
|
||||
const newSet = new SvelteSet(expandedServers);
|
||||
if (newSet.has(serverName)) {
|
||||
newSet.delete(serverName);
|
||||
if (expandedServers.has(serverName)) {
|
||||
expandedServers.delete(serverName);
|
||||
} else {
|
||||
newSet.add(serverName);
|
||||
expandedServers.add(serverName);
|
||||
}
|
||||
expandedServers = newSet;
|
||||
}
|
||||
|
||||
function toggleFolder(folderId: string) {
|
||||
const newSet = new SvelteSet(expandedFolders);
|
||||
if (newSet.has(folderId)) {
|
||||
newSet.delete(folderId);
|
||||
if (expandedFolders.has(folderId)) {
|
||||
expandedFolders.delete(folderId);
|
||||
} else {
|
||||
newSet.add(folderId);
|
||||
expandedFolders.add(folderId);
|
||||
}
|
||||
expandedFolders = newSet;
|
||||
}
|
||||
|
||||
interface ResourceTreeNode {
|
||||
|
|
@ -269,7 +259,8 @@
|
|||
{#if onToggle}
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => handleCheckboxChange(resource, checked === true)}
|
||||
onCheckedChange={(checked: boolean | 'indeterminate') =>
|
||||
handleCheckboxChange(resource, checked === true)}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
{/if}
|
||||
|
|
@ -279,23 +270,19 @@
|
|||
'hover:bg-muted/50',
|
||||
isSelected && 'bg-muted'
|
||||
)}
|
||||
onclick={(e) => handleResourceClick(resource, e)}
|
||||
onclick={(e: MouseEvent) => handleResourceClick(resource, e)}
|
||||
title={displayName}
|
||||
>
|
||||
<ResourceIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="min-w-0 flex-1 text-left">
|
||||
<TruncatedText text={displayName} />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="z-[9999]">
|
||||
<p>{displayName}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<span class="min-w-0 flex-1 truncate text-left">
|
||||
{displayName}
|
||||
</span>
|
||||
{#if onAttach}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-5 px-1.5 text-xs opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100"
|
||||
onclick={(e) => handleAttachClick(e, resource)}
|
||||
onclick={(e: MouseEvent) => handleAttachClick(e, resource)}
|
||||
>
|
||||
Attach
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ColorMode } from '$lib/enums/ui';
|
||||
import { Monitor, Moon, Sun } from '@lucide/svelte';
|
||||
|
||||
export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> = {
|
||||
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value.
|
||||
|
|
@ -132,3 +133,9 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
|
|||
enableContinueGeneration:
|
||||
'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.'
|
||||
};
|
||||
|
||||
export const SETTINGS_COLOR_MODES_CONFIG = [
|
||||
{ value: ColorMode.SYSTEM, label: 'System', icon: Monitor },
|
||||
{ value: ColorMode.LIGHT, label: 'Light', icon: Sun },
|
||||
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
|
||||
];
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import {
|
|||
MAX_INACTIVE_CONVERSATION_STATES,
|
||||
INACTIVE_CONVERSATION_STATE_MAX_AGE_MS
|
||||
} from '$lib/constants/cache';
|
||||
import { isActiveConversation } from '$lib/stores/shared';
|
||||
import type {
|
||||
ChatMessageTimings,
|
||||
ChatMessagePromptProgress,
|
||||
|
|
@ -85,20 +84,20 @@ class ChatStore {
|
|||
this.touchConversationState(convId);
|
||||
if (loading) {
|
||||
this.chatLoadingStates.set(convId, true);
|
||||
if (isActiveConversation(convId)) this.isLoading = true;
|
||||
if (convId === conversationsStore.activeConversation?.id) this.isLoading = true;
|
||||
} else {
|
||||
this.chatLoadingStates.delete(convId);
|
||||
if (isActiveConversation(convId)) this.isLoading = false;
|
||||
if (convId === conversationsStore.activeConversation?.id) this.isLoading = false;
|
||||
}
|
||||
}
|
||||
private setChatStreaming(convId: string, response: string, messageId: string): void {
|
||||
this.touchConversationState(convId);
|
||||
this.chatStreamingStates.set(convId, { response, messageId });
|
||||
if (isActiveConversation(convId)) this.currentResponse = response;
|
||||
if (convId === conversationsStore.activeConversation?.id) this.currentResponse = response;
|
||||
}
|
||||
private clearChatStreaming(convId: string): void {
|
||||
this.chatStreamingStates.delete(convId);
|
||||
if (isActiveConversation(convId)) this.currentResponse = '';
|
||||
if (convId === conversationsStore.activeConversation?.id) this.currentResponse = '';
|
||||
}
|
||||
private getChatStreaming(convId: string): { response: string; messageId: string } | undefined {
|
||||
return this.chatStreamingStates.get(convId);
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* Shared Active Conversation State
|
||||
*
|
||||
* This module provides a dependency-free shared state for tracking the active conversation.
|
||||
* It eliminates circular dependencies between chatStore and conversationsStore.
|
||||
*
|
||||
* **Why this exists:**
|
||||
* - chatStore needs to know the active conversation ID to sync global loading/streaming state
|
||||
* - conversationsStore manages the active conversation
|
||||
* - Direct imports between stores would create circular dependencies
|
||||
*
|
||||
* **Usage:**
|
||||
* - conversationsStore: calls setId() when switching conversations
|
||||
* - chatStore: calls isActive() to check if state should sync to global
|
||||
*/
|
||||
|
||||
class ActiveConversationStore {
|
||||
private _id = $state<string | null>(null);
|
||||
|
||||
/**
|
||||
* Get the currently active conversation ID.
|
||||
* Returns null if no conversation is active.
|
||||
*/
|
||||
get id(): string | null {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active conversation ID.
|
||||
* Should only be called by conversationsStore when switching conversations.
|
||||
*/
|
||||
setId(id: string | null): void {
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given conversation ID is the currently active one.
|
||||
*/
|
||||
isActive(convId: string): boolean {
|
||||
return this._id === convId;
|
||||
}
|
||||
}
|
||||
|
||||
export const activeConversationStore = new ActiveConversationStore();
|
||||
|
||||
// Convenience exports for backward compatibility
|
||||
export const getActiveConversationId = () => activeConversationStore.id;
|
||||
export const setActiveConversationId = (id: string | null) => activeConversationStore.setId(id);
|
||||
export const isActiveConversation = (convId: string) => activeConversationStore.isActive(convId);
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
/**
|
||||
* Shared State Modules
|
||||
*
|
||||
* This directory contains dependency-free state modules that can be safely
|
||||
* imported by any store without creating circular dependencies.
|
||||
*
|
||||
* **Rules for modules in this folder:**
|
||||
* - NO imports from other stores
|
||||
* - NO imports from clients or services
|
||||
* - Only pure reactive state with no business logic
|
||||
*/
|
||||
|
||||
export {
|
||||
activeConversationStore,
|
||||
getActiveConversationId,
|
||||
setActiveConversationId,
|
||||
isActiveConversation
|
||||
} from './active-conversation.svelte';
|
||||
Loading…
Reference in New Issue