Spaces:
Running
Running
File size: 7,180 Bytes
d3ab1f5 bb21933 10a8f02 bb21933 e8700b8 bb21933 d3ab1f5 e8700b8 2961a5b d3ab1f5 3cd60cd 2afbe7d e8700b8 d3ab1f5 2961a5b d3ab1f5 2961a5b bb21933 10a8f02 d3ab1f5 e8700b8 bb21933 2961a5b 843cdb6 2961a5b 5fc0417 2961a5b 5fc0417 b9ff581 2961a5b 5fc0417 2961a5b 3cd60cd bb21933 10a8f02 bb21933 9c57b89 bb21933 b9ff581 10a8f02 d218b97 10a8f02 d3ab1f5 2961a5b bb21933 10a8f02 0f2416e bb21933 f5dc173 5fc0417 dd979ff 72b54f8 5fc0417 2961a5b 5fc0417 1828161 5fc0417 3cd60cd 5fc0417 843cdb6 2afbe7d 5e1ca8c 955bb3b 2afbe7d 955bb3b 2afbe7d 955bb3b 3cd60cd d0595f7 3cd60cd d0595f7 955bb3b 843cdb6 955bb3b 843cdb6 2961a5b bb21933 d3ab1f5 bb21933 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | <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, getProviderName } from '$lib';
import { modelsState } from '$lib/state/models.svelte';
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 inferenceProvider = $derived(
getProviderName((nodeData.current?.data.inferenceProvider as string) ?? 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()}
{
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} nodeId={id} />
</div>
{/if}
</div>
{#if usage && !loading && message}
{@const modelSettings = modelsState.models.find((m) => m.id === selectedModel)}
{@const provider = modelSettings?.provider ?? 'preferred'}
<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">·</span>
{formatUsageCost(
modelSettings?.providers.find((p) => p.provider === provider)?.pricing,
usage
)}
{/if}
{#if message.timestamp}
<span class="mx-0.5">·</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">
<img
src={`https://huggingface.co/api/avatars/${inferenceProvider}`}
alt={(nodeData.current?.data.inferenceProvider as string) ??
inferenceProvider ??
provider}
class="size-4 rounded-full"
/>
{(nodeData.current?.data.inferenceProvider as string) ??
inferenceProvider ??
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} />
|