enzostvs's picture
enzostvs HF Staff
remove log
6a6a2dc
<script lang="ts">
import {
Handle,
useNodesData,
Position,
type NodeProps,
useNodes,
useEdges,
useUpdateNodeInternals,
useSvelteFlow,
type Edge,
type Node
} from '@xyflow/svelte';
import { MessageCirclePlus } from '@lucide/svelte';
import { mode } from 'mode-watcher';
import type { ChatMessage, TokenUsage } from '$lib/helpers/types';
import { Button } from '$lib/components/ui/button';
import Message from './Message.svelte';
import Spinner from '$lib/components/loading/Spinner.svelte';
import { formatUsageCost } from '$lib';
import { modelsState } from '$lib/state/models.svelte';
import { PROVIDER_SELECTION_MODES } from '$lib/consts';
import ListModels from '$lib/components/model/ListModels.svelte';
let { id }: NodeProps = $props();
// svelte-ignore state_referenced_locally
const nodeData = useNodesData(id);
const { update: updateNodes } = useNodes();
const { update: updateEdges } = useEdges();
const updateNodeInternals = useUpdateNodeInternals();
const { fitView } = useSvelteFlow();
let selectedModel = $derived((nodeData.current?.data.selectedModel as string) ?? '');
let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
let loading = $derived((nodeData.current?.data.loading as boolean) ?? false);
let usage = $derived((nodeData.current?.data.usage as TokenUsage) ?? null);
let message = $derived(
nodeData.current?.data.content != null
? ({
role: 'assistant',
content: String(nodeData.current?.data.content ?? ''),
reasoning: String(nodeData.current?.data.reasoning ?? ''),
timestamp: (nodeData.current?.data.timestamp as number) ?? 0
} as unknown as ChatMessage)
: null
);
let containerRef: HTMLDivElement | null = $state(null);
let articleRef: HTMLElement | null = $state(null);
let selectedText = $state<string | null>(null);
let selectedTextPosition = $state<{ y: number } | null>(null);
// $effect(() => {
// document.addEventListener('selectionchange', handleTextSelectionChange);
// return () => {
// document.removeEventListener('selectionchange', handleTextSelectionChange);
// };
// });
function handleTextSelectionChange(e: Event) {
const selection = document.getSelection();
const text = selection?.toString();
if (!text || text.trim() === '') {
selectedText = null;
return;
}
if (containerRef?.contains(selection?.anchorNode as unknown as HTMLElement)) {
selectedText = text;
let rect = selection?.getRangeAt(0).getBoundingClientRect();
if (rect) {
selectedTextPosition = {
y: rect.top - containerRef.getBoundingClientRect().top
};
} else {
selectedTextPosition = {
y:
(selection?.anchorNode?.parentElement?.getBoundingClientRect().top ?? 0) -
containerRef.getBoundingClientRect().top
};
}
} else {
selectedText = null;
}
}
function handleAddFollowUpMessage(prompt: string) {
document.getSelection()?.removeAllRanges();
const newNodes: Node[] = [];
const newEdges: Edge[] = [];
const newNodeId = `user-follow-up-${crypto.randomUUID()}`;
const newNode: Node = {
id: newNodeId,
type: 'user-follow-up',
position: {
x: 0,
y: 0
},
data: {
role: 'user',
prompt,
selectedModels: [selectedModel],
messages: [...messages, message]
}
};
const newEdge: Edge = {
id: `edge-${crypto.randomUUID()}`,
source: id,
target: newNodeId,
sourceHandle: 's-right',
targetHandle: 't-left'
};
newNodes.push(newNode);
newEdges.push(newEdge);
updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
updateEdges((currentEdges) => [...currentEdges, ...newEdges]);
}
let fitViewTimeout: ReturnType<typeof setTimeout>;
$effect(() => {
if (!articleRef) return;
const observer = new ResizeObserver(() => {
updateNodeInternals(id);
clearTimeout(fitViewTimeout);
fitViewTimeout = setTimeout(() => {
fitView({ duration: 300 });
}, 150);
});
observer.observe(articleRef);
return () => {
observer.disconnect();
clearTimeout(fitViewTimeout);
};
});
</script>
<article
bind:this={articleRef}
class="group/assistant relative flex w-[calc(100dvw-2rem)] flex-col rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
>
<div class="nodrag pointer-events-auto flex flex-1 cursor-auto flex-col justify-between">
<div>
<header class="mb-3 flex items-center justify-between">
<div class="flex flex-wrap items-center gap-1">
<ListModels selectedModels={[selectedModel]} showSelector={false} />
</div>
</header>
{#if loading}
<Spinner className="text-sm text-muted-foreground/70">Reaching out...</Spinner>
{/if}
{#if message}
<div bind:this={containerRef}>
<Message {message} />
</div>
{/if}
</div>
{#if usage && !loading && message}
{@const modelSettings = modelsState.models.find((m) => m.id === selectedModel)}
{@const provider = modelSettings?.provider ?? 'auto'}
<div class="mt-3 flex items-center justify-between gap-2 border-t border-border pt-3">
<p class="text-xs text-muted-foreground">
{usage.total_tokens} tokens
{#if formatUsageCost(modelSettings?.providers.find((p) => p.provider === provider)?.pricing, usage)}
<span class="mx-0.5">&middot;</span>
{formatUsageCost(
modelSettings?.providers.find((p) => p.provider === provider)?.pricing,
usage
)}
{/if}
{#if message.timestamp}
<span class="mx-0.5">&middot;</span> Latency {message.timestamp}ms
{/if}
</p>
<p class="text-xs text-muted-foreground">
<span class="inline-flex items-center gap-0.5">
Using
<span class="inline-flex items-center gap-1 rounded-full bg-muted py-0.5 pr-2 pl-1">
{#if PROVIDER_SELECTION_MODES.find((m) => m.value === provider)}
{@const mode = PROVIDER_SELECTION_MODES.find((m) => m.value === provider)!}
<span
class="inline-flex size-4 items-center justify-center rounded-full {mode.class}"
>
<mode.icon class="size-2.5 {mode.iconClass}" />
</span>
{:else}
<img
src={`https://huggingface.co/api/avatars/${provider}`}
alt={provider}
class="size-4 rounded-full"
/>
{/if}
{provider}
</span>
provider
</span>
</p>
</div>
{/if}
</div>
<Button
variant={mode.current === 'dark' ? 'default' : 'outline'}
size="icon-lg"
class="absolute top-0 right-0 z-50 translate-x-1/2 {selectedText
? 'opacity-100'
: 'pointer-events-none opacity-0'}"
style="top: {(selectedTextPosition?.y ?? 0) + 50}px;"
onclick={() => selectedText && handleAddFollowUpMessage(selectedText)}
>
<MessageCirclePlus class="size-5" />
</Button>
</article>
<Handle type="target" id="t-top" class="opacity-0" position={Position.Top} />
<Handle type="target" id="t-left" class="opacity-0" position={Position.Left} />
<Handle type="target" id="t-right" class="opacity-0" position={Position.Right} />
<Handle type="source" id="s-bottom" class="opacity-0" position={Position.Bottom} />
<Handle type="source" id="s-left" class="opacity-0" position={Position.Left} />
<Handle type="source" id="s-right" class="opacity-0" position={Position.Right} />