2626 let inputText = $state (' ' );
2727 let isGenerating = $state (false );
2828 let streamingContent = $state (' ' );
29+ let abortController: AbortController | null = $state (null );
2930 let currentSessionId = $state <string | null >(null );
3031 let currentSessionName = $state (' New Chat' );
3132 let messagesEndEl: HTMLDivElement | undefined = $state ();
@@ -250,6 +251,17 @@ Stay fully in character for the introduction, but present the dialogue hooks and
250251 viewMode = ' chat' ;
251252 }
252253
254+ // ---- Chat Control ----
255+ function stopGeneration() {
256+ if (abortController ) {
257+ abortController .abort ();
258+ abortController = null ;
259+ isGenerating = false ;
260+ streamingContent = ' ' ;
261+ console .log (' FoundryAI | Chat generation stopped by user' );
262+ }
263+ }
264+
253265 // ---- Message Sending ----
254266 async function sendMessage() {
255267 const text = inputText .trim ();
@@ -268,6 +280,7 @@ Stay fully in character for the introduction, but present the dialogue hooks and
268280 inputText = ' ' ;
269281 isGenerating = true ;
270282 streamingContent = ' ' ;
283+ abortController = new AbortController ();
271284
272285 // Add user message
273286 const userMessage: LLMMessage = { role: ' user' , content: text };
@@ -295,9 +308,9 @@ Stay fully in character for the introduction, but present the dialogue hooks and
295308 console .log (` FoundryAI | Sending message — model: ${model }, stream: ${stream }, tools: ${useTools }, messages: ${apiMessages .length }, actor: ${currentActorId || ' none' } ` );
296309
297310 if (stream ) {
298- await handleStreamingResponse (apiMessages , model , temperature , maxTokens , useTools );
311+ await handleStreamingResponse (apiMessages , model , temperature , maxTokens , useTools , abortController . signal );
299312 } else {
300- await handleNonStreamingResponse (apiMessages , model , temperature , maxTokens , useTools );
313+ await handleNonStreamingResponse (apiMessages , model , temperature , maxTokens , useTools , abortController . signal );
301314 }
302315
303316 // Save full conversation to session
@@ -310,15 +323,20 @@ Stay fully in character for the introduction, but present the dialogue hooks and
310323 console .log (` FoundryAI | Saved conversation to session ${currentSessionId } (${messages .length } messages) ` );
311324 }
312325 } catch (error : any ) {
313- console .error (' FoundryAI | Chat error:' , error );
314- const errorMsg: LLMMessage = {
315- role: ' assistant' ,
316- content: ` ⚠️ Error: ${error .message || ' Unknown error occurred' } ` ,
317- };
318- messages = [... messages , errorMsg ];
326+ if (error .name === ' AbortError' ) {
327+ console .log (' FoundryAI | Chat generation was stopped' );
328+ } else {
329+ console .error (' FoundryAI | Chat error:' , error );
330+ const errorMsg: LLMMessage = {
331+ role: ' assistant' ,
332+ content: ` ⚠️ Error: ${error .message || ' Unknown error occurred' } ` ,
333+ };
334+ messages = [... messages , errorMsg ];
335+ }
319336 } finally {
320337 isGenerating = false ;
321338 streamingContent = ' ' ;
339+ abortController = null ;
322340 }
323341 }
324342
@@ -371,6 +389,7 @@ Stay fully in character for the introduction, but present the dialogue hooks and
371389 temperature : number ,
372390 maxTokens : number ,
373391 useTools : boolean ,
392+ signal ? : AbortSignal ,
374393 ) {
375394 let fullContent = ' ' ;
376395
@@ -425,6 +444,7 @@ Stay fully in character for the introduction, but present the dialogue hooks and
425444 tool_choice: useTools ? ' auto' : undefined ,
426445 },
427446 onChunk ,
447+ signal ,
428448 );
429449
430450 if (accumulatedToolCalls .size > 0 ) {
@@ -457,15 +477,19 @@ Stay fully in character for the introduction, but present the dialogue hooks and
457477 temperature : number ,
458478 maxTokens : number ,
459479 useTools : boolean ,
480+ signal ? : AbortSignal ,
460481 ) {
461- const response = await openRouterService .chatCompletion ({
462- model ,
463- messages: apiMessages ,
464- temperature ,
465- max_tokens: maxTokens ,
466- tools: useTools ? getEnabledTools () : undefined ,
467- tool_choice: useTools ? ' auto' : undefined ,
468- });
482+ const response = await openRouterService .chatCompletion (
483+ {
484+ model ,
485+ messages: apiMessages ,
486+ temperature ,
487+ max_tokens: maxTokens ,
488+ tools: useTools ? getEnabledTools () : undefined ,
489+ tool_choice: useTools ? ' auto' : undefined ,
490+ },
491+ signal ,
492+ );
469493
470494 const assistantMessage = response .choices ?.[0 ]?.message ;
471495 console .log (' FoundryAI | Non-streaming response:' , {
@@ -911,6 +935,15 @@ Stay fully in character for the introduction, but present the dialogue hooks and
911935 <i class =" fas fa-paper-plane" ></i >
912936 {/if }
913937 </button >
938+ {#if isGenerating }
939+ <button
940+ class =" stop-btn"
941+ onclick ={stopGeneration }
942+ title =" Stop generation"
943+ >
944+ <i class =" fas fa-stop" ></i >
945+ </button >
946+ {/if }
914947 </div >
915948 {/if }
916949</div >
@@ -1368,6 +1401,28 @@ Stay fully in character for the introduction, but present the dialogue hooks and
13681401 cursor : not-allowed ;
13691402 }
13701403
1404+ .stop-btn {
1405+ background : #ef4444 ;
1406+ border : none ;
1407+ color : #fff ;
1408+ width : 38px ;
1409+ height : 38px ;
1410+ border-radius : 8px ;
1411+ cursor : pointer ;
1412+ display : flex ;
1413+ align-items : center ;
1414+ justify-content : center ;
1415+ font-size : 0.9em ;
1416+ transition : all 0.15s ;
1417+ flex-shrink : 0 ;
1418+ margin-left : 6px ;
1419+ }
1420+
1421+ .stop-btn :hover {
1422+ background : #dc2626 ;
1423+ transform : scale (1.05 );
1424+ }
1425+
13711426 /* ---- Actor Picker ---- */
13721427 .actor-picker {
13731428 display : flex ;
0 commit comments