feat: added a welcome msg for local LLM users

This commit is contained in:
Xiaohan-Tian
2026-05-14 21:24:03 -07:00
parent a48966d761
commit 1a051fea54
3 changed files with 175 additions and 8 deletions
+21
View File
@@ -0,0 +1,21 @@
## Local LLM Mode
Welcome to **K.G.Studio Musician Assistant** in local LLM mode.
- No external API calls are required. Everything runs directly in your browser, with no extra API cost.
- This mode uses **Gemma 4 E4B** through **LiteRT-LM** with **WebGPU** acceleration.
- Recommended hardware: a GPU with at least **8 GB VRAM** or a system with at least **16 GB unified RAM**.
- Performance is more limited than larger cloud-hosted models, especially on harder planning, editing, and multi-step tasks.
### Recommended Workflow
- Keep requests small and focused.
- Guide the model step by step toward the final goal.
- Work on smaller music regions instead of large full-song edits.
- Prefer simpler music arrangements when possible.
- Start a new conversation for each standalone task.
### Use an External LLM Instead
- If you want a larger cloud or self-hosted model, open **Settings -> General -> LLM Provider** and switch away from **Local LLM (Browser)**.
- For a cloud model, you can use **OpenAI**, or choose **OpenAI Compatible** and enter a provider such as OpenRouter.
- For a self-hosted model, choose **OpenAI Compatible** and enter your server's **Base URL** and **Model**.
- After switching providers, start a new conversation so the chat uses the new model cleanly.
@@ -0,0 +1,109 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { processUserMessage } from './UserMessageFilter';
import { addWelcomeMessage } from '../../utils/chatMessageUtils';
const configState = new Map<string, unknown>();
const configManagerMock = {
getIsInitialized: vi.fn(() => true),
initialize: vi.fn().mockResolvedValue(undefined),
get: vi.fn((key: string) => configState.get(key)),
};
vi.mock('../../core/config/ConfigManager', () => ({
ConfigManager: {
instance: () => configManagerMock,
},
}));
vi.mock('../chatUtil', () => ({
clearChatHistoryAndUI: vi.fn(),
}));
vi.mock('../../stores/projectStore', () => ({
useProjectStore: {
getState: () => ({
setStatus: vi.fn(),
activeRegionId: null,
selectedRegionIds: [],
}),
},
}));
vi.mock('../../agent/core/SystemPrompts', () => ({
SystemPrompts: {
getPromptWithContext: vi.fn(async (value: string) => value),
},
}));
describe('processUserMessage /welcome', () => {
beforeEach(() => {
configState.clear();
configState.set('general.llm_provider', 'local_browser');
configState.set('general.openai.api_key', '');
configState.set('general.gemini.api_key', '');
configState.set('general.claude.api_key', '');
configState.set('general.claude_openrouter.api_key', '');
configState.set('general.openai_compatible.base_url', '');
configState.set('general.openai_compatible.model', '');
configManagerMock.getIsInitialized.mockReturnValue(true);
configManagerMock.initialize.mockClear();
configManagerMock.get.mockClear();
vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request) => {
const url = String(input);
return {
ok: true,
status: 200,
text: async () => `content:${url}`,
};
}));
});
it('uses the local welcome for the local browser provider', async () => {
configState.set('general.llm_provider', 'local_browser');
configState.set('general.openai.api_key', '');
configState.set('general.openai_compatible.base_url', '');
configState.set('general.openai_compatible.model', '');
const result = await processUserMessage('/welcome');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_local_llm.md'));
expect(result.metadata).toMatchObject({ command: 'welcome', variant: 'local' });
expect(result.pseudoAssistantResponse).toContain('welcome_local_llm.md');
});
it('uses the new-user welcome for non-local providers without required config', async () => {
configState.set('general.llm_provider', 'openai');
configState.set('general.openai.api_key', '');
const result = await processUserMessage('/welcome');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_new.md'));
expect(result.metadata).toMatchObject({ command: 'welcome', variant: 'new' });
expect(result.pseudoAssistantResponse).toContain('welcome_new.md');
});
it('uses the returning-user welcome for configured non-local providers', async () => {
configState.set('general.llm_provider', 'openai_compatible');
configState.set('general.openai_compatible.base_url', 'https://openrouter.ai/api/v1');
configState.set('general.openai_compatible.model', 'qwen/qwen3-30b-a3b:free');
const result = await processUserMessage('/welcome');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_again.md'));
expect(result.metadata).toMatchObject({ command: 'welcome', variant: 'again' });
expect(result.pseudoAssistantResponse).toContain('welcome_again.md');
});
it('reuses the same welcome routing through addWelcomeMessage', async () => {
configState.set('general.llm_provider', 'local_browser');
const message = await addWelcomeMessage();
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_local_llm.md'));
expect(message?.role).toBe('assistant');
expect(message?.content).toContain('welcome_local_llm.md');
});
});
+45 -8
View File
@@ -17,6 +17,48 @@ export interface UserMessageFilterResult {
metadata?: Record<string, unknown>;
}
function hasText(value: unknown): boolean {
return typeof value === 'string' && value.trim() !== '';
}
function getWelcomeVariant(configManager: ConfigManager): 'local' | 'new' | 'again' {
const provider = (configManager.get('general.llm_provider') as string) || LOCAL_LLM_PROVIDER_KEY;
if (provider === LOCAL_LLM_PROVIDER_KEY) {
return 'local';
}
switch (provider) {
case 'openai':
return hasText(configManager.get('general.openai.api_key')) ? 'again' : 'new';
case 'gemini':
return hasText(configManager.get('general.gemini.api_key')) ? 'again' : 'new';
case 'claude':
return hasText(configManager.get('general.claude.api_key')) ? 'again' : 'new';
case 'claude_openrouter':
return hasText(configManager.get('general.claude_openrouter.api_key')) ? 'again' : 'new';
case 'openai_compatible':
return hasText(configManager.get('general.openai_compatible.base_url'))
&& hasText(configManager.get('general.openai_compatible.model'))
? 'again'
: 'new';
default:
return 'new';
}
}
function getWelcomeUrl(variant: 'local' | 'new' | 'again'): string {
switch (variant) {
case 'local':
return `${import.meta.env.BASE_URL}chat/welcome_local_llm.md`;
case 'again':
return `${import.meta.env.BASE_URL}chat/welcome_again.md`;
case 'new':
default:
return `${import.meta.env.BASE_URL}chat/welcome_new.md`;
}
}
/**
* Process a user message before it is displayed or sent to the LLM.
* Handles slash-commands and returns a structured decision.
@@ -53,12 +95,8 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
await configManager.initialize();
}
const openaiKey = (configManager.get('general.openai.api_key') as string) || '';
const oaiCompatKey = (configManager.get('general.openai_compatible.api_key') as string) || '';
const oaiCompatBaseUrl = (configManager.get('general.openai_compatible.base_url') as string) || '';
const isNew = openaiKey.trim() === '' && oaiCompatKey.trim() === '' && oaiCompatBaseUrl.trim() === '';
const url = isNew ? `${import.meta.env.BASE_URL}chat/welcome_new.md` : `${import.meta.env.BASE_URL}chat/welcome_again.md`;
const variant = getWelcomeVariant(configManager);
const url = getWelcomeUrl(variant);
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(`Failed to fetch ${url}: ${resp.status}`);
@@ -69,7 +107,7 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
sendToLLM: false,
finalMessageForLLM: null,
pseudoAssistantResponse: md,
metadata: { command: 'welcome', variant: isNew ? 'new' : 'again' }
metadata: { command: 'welcome', variant }
};
} catch (err) {
const fallback = 'Welcome to K.G.Studio Musician Assistant.';
@@ -267,4 +305,3 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
};
}
}