Skip to content

Commit 1310abb

Browse files
committed
Release 0.16.0
1 parent a6de9b6 commit 1310abb

5 files changed

Lines changed: 141 additions & 31 deletions

File tree

module.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "foundry-ai",
33
"title": "FoundryAI",
44
"description": "AI-powered DM assistant with chat interface, RAG over journals/actors, session recaps, quest tracking, and more.",
5-
"version": "0.15.0",
5+
"version": "0.16.0",
66
"authors": [
77
{
88
"name": "Derek Hearst",
@@ -30,7 +30,7 @@
3030
"socket": true,
3131
"url": "https://github.com/derekhearst/FoundryAI",
3232
"manifest": "https://github.com/derekhearst/FoundryAI/releases/latest/download/module.json",
33-
"download": "https://github.com/derekhearst/FoundryAI/releases/download/0.15.0/foundry-ai.zip",
33+
"download": "https://github.com/derekhearst/FoundryAI/releases/download/0.16.0/foundry-ai.zip",
3434
"license": "MIT",
3535
"readme": "https://github.com/derekhearst/FoundryAI/blob/main/README.md"
3636
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "foundry-ai",
3-
"version": "0.15.0",
3+
"version": "0.16.0",
44
"description": "FoundryAI - AI-powered DM assistant for Foundry VTT",
55
"type": "module",
66
"scripts": {

src/core/openrouter-service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class OpenRouterService {
168168

169169
// ---- Chat Completions ----
170170

171-
async chatCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
171+
async chatCompletion(request: ChatCompletionRequest, signal?: AbortSignal): Promise<ChatCompletionResponse> {
172172
if (!this.apiKey) throw new Error('OpenRouter API key not configured')
173173

174174
const body: ChatCompletionRequest = {
@@ -185,6 +185,7 @@ export class OpenRouterService {
185185
method: 'POST',
186186
headers: this.headers,
187187
body: JSON.stringify(body),
188+
signal,
188189
})
189190

190191
if (!response.ok) {
@@ -206,7 +207,11 @@ export class OpenRouterService {
206207
return result
207208
}
208209

209-
async chatCompletionStream(request: ChatCompletionRequest, onChunk: StreamCallback): Promise<void> {
210+
async chatCompletionStream(
211+
request: ChatCompletionRequest,
212+
onChunk: StreamCallback,
213+
signal?: AbortSignal,
214+
): Promise<void> {
210215
if (!this.apiKey) throw new Error('OpenRouter API key not configured')
211216

212217
const body: ChatCompletionRequest = {
@@ -223,6 +228,7 @@ export class OpenRouterService {
223228
method: 'POST',
224229
headers: this.headers,
225230
body: JSON.stringify(body),
231+
signal,
226232
})
227233

228234
if (!response.ok) {

src/core/tool-system.ts

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,21 +1079,69 @@ async function handleSearchJournals(query: string, maxResults?: number): Promise
10791079
}
10801080

10811081
async function handleSearchActors(query: string, maxResults?: number): Promise<string> {
1082-
const results = await embeddingService.search(query, maxResults || 5, { documentType: 'actor' })
1082+
const max = maxResults || 5
1083+
const queryLower = query.toLowerCase()
1084+
const results: Array<{ name: string; id: string; folder: string; relevance: number; excerpt: string }> = []
1085+
const allowed = getSetting('actorFolders') || []
1086+
1087+
// Get actors from the configured folders (this handles folder resolution + access control)
1088+
const indexedActors = collectionReader.getActorsByFolders(allowed)
1089+
1090+
// First pass: exact and partial name matches
1091+
const nameMatches: typeof results = []
1092+
for (const actor of indexedActors) {
1093+
const nameLower = actor.name.toLowerCase()
1094+
1095+
if (nameLower === queryLower) {
1096+
// Exact match - highest priority
1097+
nameMatches.unshift({
1098+
name: actor.name,
1099+
id: actor.id,
1100+
folder: actor.folderName,
1101+
relevance: 1.0,
1102+
excerpt: actor.content.slice(0, 500),
1103+
})
1104+
} else if (nameLower.includes(queryLower)) {
1105+
// Partial match
1106+
nameMatches.push({
1107+
name: actor.name,
1108+
id: actor.id,
1109+
folder: actor.folderName,
1110+
relevance: 0.95,
1111+
excerpt: actor.content.slice(0, 500),
1112+
})
1113+
}
1114+
1115+
if (nameMatches.length >= max) break
1116+
}
1117+
1118+
results.push(...nameMatches.slice(0, max))
1119+
1120+
// Second pass: embedding-based search if we need more results
1121+
if (results.length < max) {
1122+
const embeddingResults = await embeddingService.search(query, max - results.length, { documentType: 'actor' })
1123+
const existingIds = new Set(results.map(r => r.id))
1124+
1125+
for (const r of embeddingResults) {
1126+
if (!existingIds.has(r.entry.documentId)) {
1127+
results.push({
1128+
name: r.entry.documentName,
1129+
id: r.entry.documentId,
1130+
folder: r.entry.folderName,
1131+
relevance: Math.round(r.score * 100) / 100,
1132+
excerpt: r.entry.text.slice(0, 500),
1133+
})
1134+
1135+
if (results.length >= max) break
1136+
}
1137+
}
1138+
}
10831139

10841140
if (results.length === 0) {
10851141
return JSON.stringify({ results: [], message: 'No matching actors found.' })
10861142
}
10871143

1088-
return JSON.stringify({
1089-
results: results.map((r) => ({
1090-
documentId: r.entry.documentId,
1091-
documentName: r.entry.documentName,
1092-
folder: r.entry.folderName,
1093-
relevance: Math.round(r.score * 100) / 100,
1094-
excerpt: r.entry.text.slice(0, 500),
1095-
})),
1096-
})
1144+
return JSON.stringify({ results })
10971145
}
10981146

10991147
function handleGetJournal(journalId: string): string {
@@ -1887,6 +1935,7 @@ async function handleSearchCompendium(query: string, type?: string, maxResults?:
18871935
const results: Array<Record<string, any>> = []
18881936

18891937
for (const [packId, pack] of game.packs) {
1938+
if (!pack) continue
18901939
if (type && type !== 'all' && pack.documentName !== type) continue
18911940

18921941
// Ensure index is loaded

src/ui/components/ChatWindow.svelte

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
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

Comments
 (0)