Thomas G. Lopes commited on
Commit
9472159
·
1 Parent(s): af52a96

better styling

Browse files
package.json CHANGED
@@ -77,6 +77,7 @@
77
  },
78
  "type": "module",
79
  "dependencies": {
 
80
  "@modelcontextprotocol/sdk": "^1.13.3",
81
  "@tailwindcss/typography": "^0.5.16",
82
  "@xyflow/svelte": "^1.2.4",
 
77
  },
78
  "type": "module",
79
  "dependencies": {
80
+ "@dagrejs/dagre": "^1.1.5",
81
  "@modelcontextprotocol/sdk": "^1.13.3",
82
  "@tailwindcss/typography": "^0.5.16",
83
  "@xyflow/svelte": "^1.2.4",
pnpm-lock.yaml CHANGED
@@ -8,6 +8,9 @@ importers:
8
 
9
  .:
10
  dependencies:
 
 
 
11
  '@modelcontextprotocol/sdk':
12
  specifier: ^1.13.3
13
  version: 1.17.5
@@ -263,6 +266,13 @@ packages:
263
  resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
264
  engines: {node: '>=18'}
265
 
 
 
 
 
 
 
 
266
  '@emnapi/[email protected]':
267
  resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
268
 
@@ -3518,6 +3528,12 @@ snapshots:
3518
 
3519
  '@csstools/[email protected]': {}
3520
 
 
 
 
 
 
 
3521
  '@emnapi/[email protected]':
3522
  dependencies:
3523
  tslib: 2.8.1
 
8
 
9
  .:
10
  dependencies:
11
+ '@dagrejs/dagre':
12
+ specifier: ^1.1.5
13
+ version: 1.1.5
14
  '@modelcontextprotocol/sdk':
15
  specifier: ^1.13.3
16
  version: 1.17.5
 
266
  resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
267
  engines: {node: '>=18'}
268
 
269
+ '@dagrejs/[email protected]':
270
+ resolution: {integrity: sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==}
271
+
272
+ '@dagrejs/[email protected]':
273
+ resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==}
274
+ engines: {node: '>17.0.0'}
275
+
276
  '@emnapi/[email protected]':
277
  resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
278
 
 
3528
 
3529
  '@csstools/[email protected]': {}
3530
 
3531
+ '@dagrejs/[email protected]':
3532
+ dependencies:
3533
+ '@dagrejs/graphlib': 2.2.4
3534
+
3535
+ '@dagrejs/[email protected]': {}
3536
+
3537
  '@emnapi/[email protected]':
3538
  dependencies:
3539
  tslib: 2.8.1
src/routes/canvas/chat-node.svelte CHANGED
@@ -12,13 +12,18 @@
12
  import IconX from "~icons/lucide/x";
13
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
14
  import ModelPicker from "./model-picker.svelte";
 
 
15
 
16
- type Props = Omit<NodeProps, "data"> & { data: { query: string; response: string; modelId?: Model["id"] } };
 
 
17
  let { id, data }: Props = $props();
18
 
19
  let { updateNodeData, updateNode, getNode } = useSvelteFlow();
20
  onMount(() => {
21
  if (!data.modelId) data.modelId = models.trending[0]?.id;
 
22
  updateNode(id, { height: undefined });
23
  });
24
 
@@ -80,7 +85,8 @@
80
  });
81
 
82
  const stream = client.chatCompletionStream({
83
- provider: "auto",
 
84
  model: data.modelId,
85
  messages,
86
  temperature: 0.5,
@@ -97,15 +103,24 @@
97
  isLoading = false;
98
  }
99
  }
 
 
 
100
  </script>
101
 
102
  <div
103
- class="chat-node relative flex h-full min-h-[200px] w-full max-w-[500px] min-w-[300px]
104
- flex-col items-stretch rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"
 
105
  >
106
- <!-- Model selector -->
107
- <div class="mb-4">
108
  <ModelPicker modelId={data.modelId} onModelSelect={modelId => updateNodeData(id, { modelId })} />
 
 
 
 
 
109
  </div>
110
 
111
  <form class="flex flex-col gap-4" onsubmit={handleSubmit}>
@@ -159,16 +174,16 @@
159
 
160
  <!-- Add node button -->
161
  <button
162
- class="abs-x-center absolute -bottom-4 z-10 flex items-center gap-1.5
163
- rounded-full bg-black px-4 py-2 text-xs font-medium
164
- text-white shadow-sm transition-all hover:scale-[1.02]
165
  hover:bg-gray-900 focus:ring-2 focus:ring-gray-900/20 focus:outline-none active:scale-[0.98]"
166
  onclick={() => {
167
  const curr = getNode(id);
168
  const newNode: Node = {
169
  id: crypto.randomUUID(),
170
- position: { x: curr?.position.x ?? 100, y: (curr?.position.y ?? 0) + 400 },
171
- data: { query: "", response: "", modelId: data.modelId },
172
  type: "chat",
173
  width: undefined,
174
  height: undefined,
@@ -200,7 +215,11 @@
200
  </div>
201
 
202
  <Handle type="target" position={Position.Top} class="h-3 w-3 border-2 border-white bg-gray-500 shadow-sm" />
203
- <Handle type="source" position={Position.Bottom} class="h-3 w-3 border-2 border-white bg-gray-500 shadow-sm" />
 
 
 
 
204
 
205
  <!-- <NodeResizeControl minWidth={200} minHeight={150}> -->
206
  <!-- <IconResize class="absolute right-2 bottom-2" /> -->
 
12
  import IconX from "~icons/lucide/x";
13
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
14
  import ModelPicker from "./model-picker.svelte";
15
+ import ProviderPicker from "./provider-picker.svelte";
16
+ import { ElementSize } from "runed";
17
 
18
+ type Props = Omit<NodeProps, "data"> & {
19
+ data: { query: string; response: string; modelId?: Model["id"]; provider?: string };
20
+ };
21
  let { id, data }: Props = $props();
22
 
23
  let { updateNodeData, updateNode, getNode } = useSvelteFlow();
24
  onMount(() => {
25
  if (!data.modelId) data.modelId = models.trending[0]?.id;
26
+ if (!data.provider) data.provider = "auto";
27
  updateNode(id, { height: undefined });
28
  });
29
 
 
85
  });
86
 
87
  const stream = client.chatCompletionStream({
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ provider: (data.provider || "auto") as any,
90
  model: data.modelId,
91
  messages,
92
  temperature: 0.5,
 
103
  isLoading = false;
104
  }
105
  }
106
+
107
+ let node = $state<HTMLElement>();
108
+ const size = new ElementSize(() => node);
109
  </script>
110
 
111
  <div
112
+ class="chat-node group relative flex h-full min-h-[200px] w-full max-w-[800px]
113
+ min-w-[500px] flex-col items-stretch rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"
114
+ bind:this={node}
115
  >
116
+ <!-- Model and Provider selectors -->
117
+ <div class="mb-4 space-y-3">
118
  <ModelPicker modelId={data.modelId} onModelSelect={modelId => updateNodeData(id, { modelId })} />
119
+ <ProviderPicker
120
+ provider={data.provider}
121
+ modelId={data.modelId}
122
+ onProviderSelect={provider => updateNodeData(id, { provider })}
123
+ />
124
  </div>
125
 
126
  <form class="flex flex-col gap-4" onsubmit={handleSubmit}>
 
174
 
175
  <!-- Add node button -->
176
  <button
177
+ class="abs-x-center absolute -bottom-4 z-10 flex items-center gap-1.5 rounded-full bg-black
178
+ px-4 py-2 text-xs font-medium text-white opacity-0
179
+ shadow-sm transition-all group-hover:opacity-100 hover:scale-[1.02]
180
  hover:bg-gray-900 focus:ring-2 focus:ring-gray-900/20 focus:outline-none active:scale-[0.98]"
181
  onclick={() => {
182
  const curr = getNode(id);
183
  const newNode: Node = {
184
  id: crypto.randomUUID(),
185
+ position: { x: curr?.position.x ?? 100, y: (curr?.position.y ?? 0) + size.height + 40 },
186
+ data: { query: "", response: "", modelId: data.modelId, provider: data.provider },
187
  type: "chat",
188
  width: undefined,
189
  height: undefined,
 
215
  </div>
216
 
217
  <Handle type="target" position={Position.Top} class="h-3 w-3 border-2 border-white bg-gray-500 shadow-sm" />
218
+ <Handle
219
+ type="source"
220
+ position={Position.Bottom}
221
+ class="h-3 w-3 border-2 border-white bg-gray-500 opacity-0 shadow-sm"
222
+ />
223
 
224
  <!-- <NodeResizeControl minWidth={200} minHeight={150}> -->
225
  <!-- <IconResize class="absolute right-2 bottom-2" /> -->
src/routes/canvas/provider-picker.svelte ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { models } from "$lib/state/models.svelte.js";
3
+ import { pricing } from "$lib/state/pricing.svelte.js";
4
+ import { isHFModel } from "$lib/types.js";
5
+ import { Select } from "melt/builders";
6
+ import IconCaret from "~icons/carbon/chevron-down";
7
+ import IconProvider from "../../lib/components/icon-provider.svelte";
8
+
9
+ interface Props {
10
+ provider?: string;
11
+ modelId?: string;
12
+ onProviderSelect?: (provider: string) => void;
13
+ }
14
+
15
+ let { provider = "auto", modelId, onProviderSelect }: Props = $props();
16
+
17
+ const selectedModel = $derived.by(() => {
18
+ if (!modelId) return undefined;
19
+ return models.all.find(m => m.id === modelId);
20
+ });
21
+
22
+ const availableProviders = $derived.by(() => {
23
+ if (!selectedModel || !isHFModel(selectedModel)) return [{ provider: "auto", providerId: "auto" }];
24
+ if (!selectedModel.inferenceProviderMapping) return [{ provider: "auto", providerId: "auto" }];
25
+ return [...selectedModel.inferenceProviderMapping, { provider: "auto", providerId: "auto" }];
26
+ });
27
+
28
+ const select = new Select<string, false>({
29
+ value: () => provider,
30
+ onValueChange(newProvider) {
31
+ if (!newProvider) return;
32
+ onProviderSelect?.(newProvider);
33
+ },
34
+ });
35
+
36
+ const nameMap: Record<string, string> = {
37
+ "sambanova": "SambaNova",
38
+ "fal": "fal",
39
+ "cerebras": "Cerebras",
40
+ "replicate": "Replicate",
41
+ "black-forest-labs": "Black Forest Labs",
42
+ "fireworks-ai": "Fireworks",
43
+ "together": "Together AI",
44
+ "nebius": "Nebius AI Studio",
45
+ "hyperbolic": "Hyperbolic",
46
+ "novita": "Novita",
47
+ "cohere": "Cohere",
48
+ "hf-inference": "HF Inference API",
49
+ };
50
+
51
+ const UPPERCASE_WORDS = ["hf", "ai"];
52
+
53
+ function formatName(provider: string) {
54
+ if (provider in nameMap) return nameMap[provider];
55
+
56
+ const words = provider
57
+ .toLowerCase()
58
+ .split("-")
59
+ .map(word => {
60
+ if (UPPERCASE_WORDS.includes(word)) {
61
+ return word.toUpperCase();
62
+ } else {
63
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
64
+ }
65
+ });
66
+
67
+ return words.join(" ");
68
+ }
69
+
70
+ function getProviderName(provider: string) {
71
+ if (provider in nameMap) return formatName(provider);
72
+ return provider === "auto" ? "Auto" : provider;
73
+ }
74
+
75
+ function getProviderPricing(provider: string) {
76
+ if (provider === "auto" || !selectedModel) return null;
77
+ const pd = pricing.getPricing(selectedModel.id, provider);
78
+ return pricing.formatPricing(pd);
79
+ }
80
+ </script>
81
+
82
+ {#snippet providerDisplay(providerValue: string)}
83
+ {@const providerPricing = getProviderPricing(providerValue)}
84
+ <div class="flex flex-col items-start gap-0.5">
85
+ <div class="flex items-center gap-2 text-sm text-gray-900">
86
+ <IconProvider provider={providerValue} />
87
+ <span>{getProviderName(providerValue)}</span>
88
+ </div>
89
+ {#if providerPricing}
90
+ <span class="text-xs text-gray-500">
91
+ In: {providerPricing.input} • Out: {providerPricing.output}
92
+ </span>
93
+ {/if}
94
+ </div>
95
+ {/snippet}
96
+
97
+ <div class="relative w-full">
98
+ <label class="block space-y-1.5 text-xs font-medium text-gray-600">
99
+ <p>Provider</p>
100
+
101
+ <button
102
+ {...select.trigger}
103
+ class="relative flex w-full items-center justify-between gap-6 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm leading-tight whitespace-nowrap shadow-sm transition-colors hover:bg-gray-100 focus:border-gray-900 focus:ring-2 focus:ring-gray-900/10 focus:outline-none"
104
+ >
105
+ {@render providerDisplay(provider)}
106
+ <div class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs">
107
+ <IconCaret />
108
+ </div>
109
+ </button>
110
+ </label>
111
+
112
+ <div
113
+ {...select.content}
114
+ class="absolute z-50 mt-1 hidden w-full !min-w-[300px] overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg data-[open]:block"
115
+ >
116
+ {#snippet option(providerValue: string)}
117
+ <div {...select.getOption(providerValue)} class="group block w-full p-1 text-sm">
118
+ <div class="rounded-md px-2 py-1.5 group-data-[highlighted]:bg-gray-100">
119
+ {@render providerDisplay(providerValue)}
120
+ </div>
121
+ </div>
122
+ {/snippet}
123
+
124
+ {#each availableProviders as { provider: providerValue } (providerValue)}
125
+ {@render option(providerValue)}
126
+ {/each}
127
+ </div>
128
+ </div>
src/routes/canvas/state.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { Edge, Node } from "@xyflow/svelte";
2
  import { PersistedState } from "runed";
3
 
4
  export const nodes = new PersistedState<Node[]>("inf-pg-nodes", [
5
  {
6
  id: "1",
7
  position: { x: 100, y: 100 },
8
- data: { query: "", response: "", modelId: undefined },
9
  type: "chat",
10
  width: undefined,
11
  height: undefined,
 
1
+ import { type Edge, type Node } from "@xyflow/svelte";
2
  import { PersistedState } from "runed";
3
 
4
  export const nodes = new PersistedState<Node[]>("inf-pg-nodes", [
5
  {
6
  id: "1",
7
  position: { x: 100, y: 100 },
8
+ data: { query: "", response: "", modelId: undefined, provider: "auto" },
9
  type: "chat",
10
  width: undefined,
11
  height: undefined,