Thomas G. Lopes commited on
Commit
92c8678
·
1 Parent(s): c3571d4
src/routes/canvas/chat-node.svelte CHANGED
@@ -10,6 +10,7 @@
10
  import IconLoading from "~icons/lucide/loader-2";
11
  import IconAdd from "~icons/lucide/plus";
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";
@@ -32,6 +33,7 @@
32
  const autosized = new TextareaAutosize();
33
 
34
  let isLoading = $state(false);
 
35
 
36
  const history = $derived.by(function getNodeHistory() {
37
  const node = nodes.current.find(n => n.id === id);
@@ -63,6 +65,7 @@
63
  async function handleSubmit(e: SubmitEvent) {
64
  e.preventDefault();
65
  isLoading = true;
 
66
  updateNodeData(id, { response: "" });
67
 
68
  try {
@@ -86,23 +89,47 @@
86
  return res;
87
  });
88
 
89
- const stream = client.chatCompletionStream({
90
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
- provider: (data.provider || "auto") as any,
92
- model: data.modelId,
93
- messages,
94
- temperature: 0.5,
95
- top_p: 0.7,
96
- });
 
 
 
 
 
97
 
98
  for await (const chunk of stream) {
 
 
 
99
  if (chunk.choices && chunk.choices.length > 0) {
100
  const newContent = chunk.choices[0]?.delta.content ?? "";
101
  updateNodeData(id, { response: data.response + newContent });
102
  }
103
  }
 
 
 
 
 
 
 
104
  } finally {
105
  isLoading = false;
 
 
 
 
 
 
 
 
 
106
  }
107
  }
108
 
@@ -143,17 +170,17 @@
143
  </div>
144
 
145
  <button
146
- type="submit"
147
- disabled={isLoading}
148
  class="flex items-center justify-center gap-2 self-center rounded-xl
149
  bg-black px-6 py-2.5 text-sm font-medium
150
  text-white transition-all hover:scale-[1.02] hover:bg-gray-900
151
  focus:ring-2 focus:ring-gray-900/20 focus:outline-none
152
- active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50"
153
  >
154
  {#if isLoading}
155
- <IconLoading class="h-4 w-4 animate-spin" />
156
- Sending...
157
  {:else}
158
  Send Message
159
  {/if}
@@ -162,14 +189,16 @@
162
 
163
  {#if data.response || isLoading}
164
  <div class="mt-4 rounded-lg border border-gray-100 bg-gray-50/50 p-4">
165
- <div class="mb-2 text-xs font-medium text-gray-600">Response</div>
 
 
 
 
 
166
  {#if data.response}
167
  <pre class="text-sm leading-relaxed whitespace-pre-wrap text-gray-800">{data.response}</pre>
168
  {:else if isLoading}
169
- <div class="flex items-center gap-2 text-sm text-gray-500">
170
- <IconLoading class="h-4 w-4 animate-spin" />
171
- Generating response...
172
- </div>
173
  {/if}
174
  </div>
175
  {/if}
 
10
  import IconLoading from "~icons/lucide/loader-2";
11
  import IconAdd from "~icons/lucide/plus";
12
  import IconX from "~icons/lucide/x";
13
+ import IconStop from "~icons/lucide/square";
14
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
15
  import ModelPicker from "./model-picker.svelte";
16
  import ProviderPicker from "./provider-picker.svelte";
 
33
  const autosized = new TextareaAutosize();
34
 
35
  let isLoading = $state(false);
36
+ let abortController = $state<AbortController | null>(null);
37
 
38
  const history = $derived.by(function getNodeHistory() {
39
  const node = nodes.current.find(n => n.id === id);
 
65
  async function handleSubmit(e: SubmitEvent) {
66
  e.preventDefault();
67
  isLoading = true;
68
+ abortController = new AbortController();
69
  updateNodeData(id, { response: "" });
70
 
71
  try {
 
89
  return res;
90
  });
91
 
92
+ const stream = client.chatCompletionStream(
93
+ {
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ provider: (data.provider || "auto") as any,
96
+ model: data.modelId,
97
+ messages,
98
+ temperature: 0.5,
99
+ top_p: 0.7,
100
+ },
101
+ {
102
+ signal: abortController.signal,
103
+ },
104
+ );
105
 
106
  for await (const chunk of stream) {
107
+ if (abortController.signal.aborted) {
108
+ break;
109
+ }
110
  if (chunk.choices && chunk.choices.length > 0) {
111
  const newContent = chunk.choices[0]?.delta.content ?? "";
112
  updateNodeData(id, { response: data.response + newContent });
113
  }
114
  }
115
+ } catch (error) {
116
+ if (error instanceof Error && error.name === "AbortError") {
117
+ // Generation was aborted, this is expected
118
+ return;
119
+ }
120
+ // Re-throw other errors
121
+ throw error;
122
  } finally {
123
  isLoading = false;
124
+ abortController = null;
125
+ }
126
+ }
127
+
128
+ function stopGeneration(e: MouseEvent) {
129
+ e.stopPropagation();
130
+ e.preventDefault();
131
+ if (abortController) {
132
+ abortController.abort();
133
  }
134
  }
135
 
 
170
  </div>
171
 
172
  <button
173
+ type={isLoading ? "button" : "submit"}
174
+ onclick={isLoading ? stopGeneration : undefined}
175
  class="flex items-center justify-center gap-2 self-center rounded-xl
176
  bg-black px-6 py-2.5 text-sm font-medium
177
  text-white transition-all hover:scale-[1.02] hover:bg-gray-900
178
  focus:ring-2 focus:ring-gray-900/20 focus:outline-none
179
+ active:scale-[0.98]"
180
  >
181
  {#if isLoading}
182
+ <IconStop class="h-4 w-4" />
183
+ Stop Generation
184
  {:else}
185
  Send Message
186
  {/if}
 
189
 
190
  {#if data.response || isLoading}
191
  <div class="mt-4 rounded-lg border border-gray-100 bg-gray-50/50 p-4">
192
+ <div class="mb-2 flex items-center gap-2 text-xs font-medium text-gray-600">
193
+ Response
194
+ {#if isLoading}
195
+ <IconLoading class="h-3 w-3 animate-spin" />
196
+ {/if}
197
+ </div>
198
  {#if data.response}
199
  <pre class="text-sm leading-relaxed whitespace-pre-wrap text-gray-800">{data.response}</pre>
200
  {:else if isLoading}
201
+ <div class="text-sm text-gray-500">Generating response...</div>
 
 
 
202
  {/if}
203
  </div>
204
  {/if}
src/routes/canvas/model-picker.svelte CHANGED
@@ -151,6 +151,9 @@
151
  e.preventDefault();
152
  virtualScroll.container.onscroll(e);
153
  }}
 
 
 
154
  >
155
  <!-- Virtualized model list -->
156
  {#snippet modelEntry(model: Model | CustomModel, trending?: boolean)}
 
151
  e.preventDefault();
152
  virtualScroll.container.onscroll(e);
153
  }}
154
+ onwheel={e => {
155
+ e.stopPropagation();
156
+ }}
157
  >
158
  <!-- Virtualized model list -->
159
  {#snippet modelEntry(model: Model | CustomModel, trending?: boolean)}