Spaces:
Runtime error
Runtime error
Thomas G. Lopes
commited on
Commit
·
9b3602c
1
Parent(s):
a261aa8
functional poc
Browse files- src/routes/canvas/+page.svelte +9 -18
- src/routes/canvas/chat-node.svelte +87 -15
- src/routes/canvas/state.ts +15 -0
src/routes/canvas/+page.svelte
CHANGED
|
@@ -1,26 +1,11 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import { Background, Controls, MiniMap, SvelteFlow
|
| 3 |
-
|
| 4 |
import "@xyflow/svelte/dist/style.css";
|
| 5 |
import ChatNode from "./chat-node.svelte";
|
| 6 |
-
import {
|
| 7 |
|
| 8 |
const nodeTypes = { chat: ChatNode } as const;
|
| 9 |
|
| 10 |
-
let nodes = new PersistedState<Node[]>("inf-pg-nodes", [
|
| 11 |
-
{
|
| 12 |
-
id: "1",
|
| 13 |
-
position: { x: 100, y: 100 },
|
| 14 |
-
data: { query: "", response: "", modelId: undefined },
|
| 15 |
-
type: "chat",
|
| 16 |
-
width: undefined,
|
| 17 |
-
height: undefined,
|
| 18 |
-
},
|
| 19 |
-
]);
|
| 20 |
-
$inspect(nodes);
|
| 21 |
-
|
| 22 |
-
let edges = $state.raw<Edge[]>([]);
|
| 23 |
-
|
| 24 |
// Make edges non-editable
|
| 25 |
const edgeOptions = {
|
| 26 |
deletable: false,
|
|
@@ -29,7 +14,13 @@
|
|
| 29 |
</script>
|
| 30 |
|
| 31 |
<div style:width="100vw" style:height="100vh">
|
| 32 |
-
<SvelteFlow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
<MiniMap />
|
| 34 |
<Controls />
|
| 35 |
<Background />
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { Background, Controls, MiniMap, SvelteFlow } from "@xyflow/svelte";
|
|
|
|
| 3 |
import "@xyflow/svelte/dist/style.css";
|
| 4 |
import ChatNode from "./chat-node.svelte";
|
| 5 |
+
import { edges, nodes } from "./state.js";
|
| 6 |
|
| 7 |
const nodeTypes = { chat: ChatNode } as const;
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
// Make edges non-editable
|
| 10 |
const edgeOptions = {
|
| 11 |
deletable: false,
|
|
|
|
| 14 |
</script>
|
| 15 |
|
| 16 |
<div style:width="100vw" style:height="100vh">
|
| 17 |
+
<SvelteFlow
|
| 18 |
+
bind:nodes={nodes.current}
|
| 19 |
+
bind:edges={edges.current}
|
| 20 |
+
fitView
|
| 21 |
+
{nodeTypes}
|
| 22 |
+
defaultEdgeOptions={edgeOptions}
|
| 23 |
+
>
|
| 24 |
<MiniMap />
|
| 25 |
<Controls />
|
| 26 |
<Background />
|
src/routes/canvas/chat-node.svelte
CHANGED
|
@@ -4,36 +4,76 @@
|
|
| 4 |
import { token } from "$lib/state/token.svelte";
|
| 5 |
import type { Model } from "$lib/types.js";
|
| 6 |
import { InferenceClient } from "@huggingface/inference";
|
| 7 |
-
import { Handle, Position, useSvelteFlow, type NodeProps } from "@xyflow/svelte";
|
| 8 |
import { onMount } from "svelte";
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
type Props = NodeProps & { data: { query: string; response: string; modelId?: Model["id"] } };
|
| 11 |
let { id, data }: Props = $props();
|
| 12 |
|
| 13 |
-
let { updateNodeData, updateNode } = useSvelteFlow();
|
| 14 |
onMount(() => {
|
| 15 |
if (!data.modelId) data.modelId = models.trending[0]?.id;
|
| 16 |
updateNode(id, { height: undefined });
|
| 17 |
});
|
| 18 |
|
| 19 |
-
let modelId = $state<Model["id"] | undefined>(models.trending[0]?.id);
|
| 20 |
-
|
| 21 |
const autosized = new TextareaAutosize();
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
async function handleSubmit(e: SubmitEvent) {
|
| 24 |
e.preventDefault();
|
| 25 |
updateNodeData(id, { response: "" });
|
| 26 |
const client = new InferenceClient(token.value);
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
const stream = client.chatCompletionStream({
|
| 29 |
provider: "auto",
|
| 30 |
model: data.modelId,
|
| 31 |
-
messages
|
| 32 |
-
{
|
| 33 |
-
role: "user",
|
| 34 |
-
content: data.query,
|
| 35 |
-
},
|
| 36 |
-
],
|
| 37 |
temperature: 0.5,
|
| 38 |
top_p: 0.7,
|
| 39 |
});
|
|
@@ -45,14 +85,13 @@
|
|
| 45 |
}
|
| 46 |
}
|
| 47 |
}
|
| 48 |
-
$inspect(data);
|
| 49 |
</script>
|
| 50 |
|
| 51 |
<div
|
| 52 |
-
class="chat-node flex h-full min-h-[150px] w-full min-w-[200px]
|
| 53 |
-
items-stretch rounded border bg-white p-8 shadow-xs"
|
| 54 |
>
|
| 55 |
-
<select class="block border" bind:value={modelId}>
|
| 56 |
{#each models.all as model}
|
| 57 |
<option value={model.id}>{model.id}</option>
|
| 58 |
{/each}
|
|
@@ -76,6 +115,39 @@
|
|
| 76 |
<pre class="whitespace-pre-wrap">{data.response}</pre>
|
| 77 |
</div>
|
| 78 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
</div>
|
| 80 |
|
| 81 |
<Handle type="target" position={Position.Top} />
|
|
|
|
| 4 |
import { token } from "$lib/state/token.svelte";
|
| 5 |
import type { Model } from "$lib/types.js";
|
| 6 |
import { InferenceClient } from "@huggingface/inference";
|
| 7 |
+
import { Handle, Position, useSvelteFlow, type Edge, type Node, type NodeProps } from "@xyflow/svelte";
|
| 8 |
import { onMount } from "svelte";
|
| 9 |
+
import { edges, nodes } from "./state.js";
|
| 10 |
+
import IconCross from "~icons/carbon/x";
|
| 11 |
+
import type { ChatCompletionInputMessage } from "@huggingface/tasks";
|
| 12 |
|
| 13 |
+
type Props = Omit<NodeProps, "data"> & { data: { query: string; response: string; modelId?: Model["id"] } };
|
| 14 |
let { id, data }: Props = $props();
|
| 15 |
|
| 16 |
+
let { updateNodeData, updateNode, getNode } = useSvelteFlow();
|
| 17 |
onMount(() => {
|
| 18 |
if (!data.modelId) data.modelId = models.trending[0]?.id;
|
| 19 |
updateNode(id, { height: undefined });
|
| 20 |
});
|
| 21 |
|
|
|
|
|
|
|
| 22 |
const autosized = new TextareaAutosize();
|
| 23 |
|
| 24 |
+
const history = $derived.by(function getNodeHistory() {
|
| 25 |
+
const node = nodes.current.find(n => n.id === id);
|
| 26 |
+
if (!node) return [];
|
| 27 |
+
|
| 28 |
+
let history: Array<Omit<Node, "data"> & { data: Props["data"] }> = [node as any];
|
| 29 |
+
let target = node.id;
|
| 30 |
+
|
| 31 |
+
while (true) {
|
| 32 |
+
const parentEdge = edges.current.find(edge => edge.target === target);
|
| 33 |
+
if (!parentEdge) break; // No more parents found
|
| 34 |
+
|
| 35 |
+
const parentNode = nodes.current.find(n => n.id === parentEdge.source);
|
| 36 |
+
if (!parentNode) {
|
| 37 |
+
// Optional: clean up broken edges
|
| 38 |
+
// edges.current = edges.current.filter(e => e.id !== parentEdge.id);
|
| 39 |
+
break;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
history.unshift(parentNode as any);
|
| 43 |
+
target = parentNode.id; // Move up the chain
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
return history;
|
| 47 |
+
});
|
| 48 |
+
$inspect(data.query, history);
|
| 49 |
+
|
| 50 |
async function handleSubmit(e: SubmitEvent) {
|
| 51 |
e.preventDefault();
|
| 52 |
updateNodeData(id, { response: "" });
|
| 53 |
const client = new InferenceClient(token.value);
|
| 54 |
|
| 55 |
+
const messages: ChatCompletionInputMessage[] = history.flatMap(n => {
|
| 56 |
+
const res: ChatCompletionInputMessage[] = [];
|
| 57 |
+
if (n.data.query) {
|
| 58 |
+
res.push({
|
| 59 |
+
role: "user",
|
| 60 |
+
content: n.data.query,
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
if (n.data.response) {
|
| 64 |
+
res.push({
|
| 65 |
+
role: "assistant",
|
| 66 |
+
content: n.data.response,
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
return res;
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
const stream = client.chatCompletionStream({
|
| 74 |
provider: "auto",
|
| 75 |
model: data.modelId,
|
| 76 |
+
messages,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
temperature: 0.5,
|
| 78 |
top_p: 0.7,
|
| 79 |
});
|
|
|
|
| 85 |
}
|
| 86 |
}
|
| 87 |
}
|
|
|
|
| 88 |
</script>
|
| 89 |
|
| 90 |
<div
|
| 91 |
+
class="chat-node relative flex h-full min-h-[150px] w-full max-w-[500px] min-w-[200px]
|
| 92 |
+
flex-col items-stretch rounded border bg-white p-8 shadow-xs"
|
| 93 |
>
|
| 94 |
+
<select class="block border" bind:value={() => data.modelId, modelId => updateNodeData(id, { modelId })}>
|
| 95 |
{#each models.all as model}
|
| 96 |
<option value={model.id}>{model.id}</option>
|
| 97 |
{/each}
|
|
|
|
| 115 |
<pre class="whitespace-pre-wrap">{data.response}</pre>
|
| 116 |
</div>
|
| 117 |
{/if}
|
| 118 |
+
|
| 119 |
+
<!-- Add node -->
|
| 120 |
+
<button
|
| 121 |
+
class="abs-x-center absolute bottom-0 z-10 translate-y-1/2 rounded bg-black px-3 py-1
|
| 122 |
+
text-xs text-white hover:bg-neutral-700"
|
| 123 |
+
onclick={() => {
|
| 124 |
+
const curr = getNode(id);
|
| 125 |
+
const newNode: Node = {
|
| 126 |
+
id: crypto.randomUUID(),
|
| 127 |
+
position: { x: curr?.position.x ?? 100, y: (curr?.position.y ?? 0) + 500 },
|
| 128 |
+
data: { query: "", response: "", modelId: data.modelId },
|
| 129 |
+
type: "chat",
|
| 130 |
+
width: undefined,
|
| 131 |
+
height: undefined,
|
| 132 |
+
};
|
| 133 |
+
nodes.current.push(newNode);
|
| 134 |
+
const edge: Edge = {
|
| 135 |
+
id: crypto.randomUUID(),
|
| 136 |
+
source: curr!.id,
|
| 137 |
+
target: newNode.id,
|
| 138 |
+
animated: true,
|
| 139 |
+
label: "",
|
| 140 |
+
data: {},
|
| 141 |
+
};
|
| 142 |
+
edges.current.push(edge);
|
| 143 |
+
}}
|
| 144 |
+
>
|
| 145 |
+
Add
|
| 146 |
+
</button>
|
| 147 |
+
|
| 148 |
+
<button class="absolute top-1 right-1" onclick={() => (nodes.current = nodes.current.filter(n => n.id !== id))}>
|
| 149 |
+
<IconCross />
|
| 150 |
+
</button>
|
| 151 |
</div>
|
| 152 |
|
| 153 |
<Handle type="target" position={Position.Top} />
|
src/routes/canvas/state.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 12 |
+
},
|
| 13 |
+
]);
|
| 14 |
+
|
| 15 |
+
export const edges = new PersistedState<Edge[]>("inf-pg-edges", []);
|