feat: added a welcome msg for local LLM users
This commit is contained in:
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,6 +17,48 @@ export interface UserMessageFilterResult {
|
|||||||
metadata?: Record<string, unknown>;
|
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.
|
* Process a user message before it is displayed or sent to the LLM.
|
||||||
* Handles slash-commands and returns a structured decision.
|
* Handles slash-commands and returns a structured decision.
|
||||||
@@ -53,12 +95,8 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
|
|||||||
await configManager.initialize();
|
await configManager.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
const openaiKey = (configManager.get('general.openai.api_key') as string) || '';
|
const variant = getWelcomeVariant(configManager);
|
||||||
const oaiCompatKey = (configManager.get('general.openai_compatible.api_key') as string) || '';
|
const url = getWelcomeUrl(variant);
|
||||||
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 resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error(`Failed to fetch ${url}: ${resp.status}`);
|
throw new Error(`Failed to fetch ${url}: ${resp.status}`);
|
||||||
@@ -69,7 +107,7 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
|
|||||||
sendToLLM: false,
|
sendToLLM: false,
|
||||||
finalMessageForLLM: null,
|
finalMessageForLLM: null,
|
||||||
pseudoAssistantResponse: md,
|
pseudoAssistantResponse: md,
|
||||||
metadata: { command: 'welcome', variant: isNew ? 'new' : 'again' }
|
metadata: { command: 'welcome', variant }
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const fallback = 'Welcome to K.G.Studio Musician Assistant.';
|
const fallback = 'Welcome to K.G.Studio Musician Assistant.';
|
||||||
@@ -267,4 +305,3 @@ export async function processUserMessage(originalMessage: string): Promise<UserM
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user