mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
feat: Support project-based feature.
This commit is contained in:
parent
e165288a59
commit
1694357823
34 changed files with 1915 additions and 175 deletions
150
package-lock.json
generated
150
package-lock.json
generated
|
|
@ -35,6 +35,7 @@
|
|||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@tabler/icons-react": "^2.14.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
|
|
@ -6518,6 +6519,155 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
|
||||
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-roving-focus": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
||||
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz",
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@tabler/icons-react": "^2.14.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import {
|
||||
getChainType,
|
||||
getCurrentProject,
|
||||
getModelKey,
|
||||
SetChainOptions,
|
||||
setChainType,
|
||||
subscribeToChainTypeChange,
|
||||
subscribeToModelKeyChange,
|
||||
} from "@/aiParams";
|
||||
import ChainFactory, { ChainType, Document } from "@/chainFactory";
|
||||
import { BUILTIN_CHAT_MODELS, USER_SENDER, VAULT_VECTOR_STORE_STRATEGY } from "@/constants";
|
||||
import { BUILTIN_CHAT_MODELS, USER_SENDER } from "@/constants";
|
||||
import {
|
||||
ChainRunner,
|
||||
CopilotPlusChainRunner,
|
||||
LLMChainRunner,
|
||||
ProjectChainRunner,
|
||||
VaultQAChainRunner,
|
||||
} from "@/LLMProviders/chainRunner";
|
||||
import { logError, logInfo } from "@/logger";
|
||||
|
|
@ -32,17 +32,27 @@ import MemoryManager from "./memoryManager";
|
|||
import PromptManager from "./promptManager";
|
||||
|
||||
export default class ChainManager {
|
||||
private static chain: RunnableSequence;
|
||||
private static retrievalChain: RunnableSequence;
|
||||
private chain: RunnableSequence;
|
||||
private retrievalChain: RunnableSequence;
|
||||
private retrievedDocuments: Document[] = [];
|
||||
|
||||
public getRetrievedDocuments(): Document[] {
|
||||
return this.retrievedDocuments;
|
||||
}
|
||||
|
||||
public app: App;
|
||||
public vectorStoreManager: VectorStoreManager;
|
||||
public chatModelManager: ChatModelManager;
|
||||
public memoryManager: MemoryManager;
|
||||
public promptManager: PromptManager;
|
||||
public static retrievedDocuments: Document[] = [];
|
||||
|
||||
// A chat history that stores the messages sent and received
|
||||
// Only reset when the user explicitly clicks "New Chat"
|
||||
private chatMessages: ChatMessage[] = [];
|
||||
|
||||
constructor(app: App, vectorStoreManager: VectorStoreManager) {
|
||||
this.chatMessages = [];
|
||||
|
||||
// Instantiate singletons
|
||||
this.app = app;
|
||||
this.vectorStoreManager = vectorStoreManager;
|
||||
|
|
@ -53,29 +63,21 @@ export default class ChainManager {
|
|||
// Initialize async operations
|
||||
this.initialize();
|
||||
|
||||
// Set up subscriptions
|
||||
subscribeToModelKeyChange(async () => await this.createChainWithNewModel());
|
||||
subscribeToChainTypeChange(() =>
|
||||
this.setChain(getChainType(), {
|
||||
refreshIndex:
|
||||
getSettings().indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH &&
|
||||
(getChainType() === ChainType.VAULT_QA_CHAIN ||
|
||||
getChainType() === ChainType.COPILOT_PLUS_CHAIN),
|
||||
})
|
||||
);
|
||||
subscribeToSettingsChange(async () => await this.createChainWithNewModel());
|
||||
subscribeToSettingsChange(async () => {
|
||||
await this.createChainWithNewModel();
|
||||
});
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
await this.createChainWithNewModel();
|
||||
}
|
||||
|
||||
static getChain(): RunnableSequence {
|
||||
return ChainManager.chain;
|
||||
public getChain(): RunnableSequence {
|
||||
return this.chain;
|
||||
}
|
||||
|
||||
static getRetrievalChain(): RunnableSequence {
|
||||
return ChainManager.retrievalChain;
|
||||
public getRetrievalChain(): RunnableSequence {
|
||||
return this.retrievalChain;
|
||||
}
|
||||
|
||||
private validateChainType(chainType: ChainType): void {
|
||||
|
|
@ -92,35 +94,60 @@ export default class ChainManager {
|
|||
}
|
||||
|
||||
private validateChainInitialization() {
|
||||
if (!ChainManager.chain || !isSupportedChain(ChainManager.chain)) {
|
||||
if (!this.chain || !isSupportedChain(this.chain)) {
|
||||
console.error("Chain is not initialized properly, re-initializing chain: ", getChainType());
|
||||
this.setChain(getChainType());
|
||||
this.createChainWithNewModel({}, false);
|
||||
// this.setChain(getChainType());
|
||||
}
|
||||
}
|
||||
|
||||
static storeRetrieverDocuments(documents: Document[]) {
|
||||
ChainManager.retrievedDocuments = documents;
|
||||
public storeRetrieverDocuments(documents: Document[]) {
|
||||
this.retrievedDocuments = documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the active model and create a new chain with the specified model
|
||||
* name.
|
||||
*/
|
||||
async createChainWithNewModel(): Promise<void> {
|
||||
let newModelKey = getModelKey();
|
||||
async createChainWithNewModel(
|
||||
options: SetChainOptions = {},
|
||||
neededReInitChatMode: boolean = true
|
||||
): Promise<void> {
|
||||
const chainType = getChainType();
|
||||
const currentProject = getCurrentProject();
|
||||
|
||||
if (chainType === ChainType.PROJECT_CHAIN && !currentProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newModelKey =
|
||||
chainType === ChainType.PROJECT_CHAIN ? currentProject?.projectModelKey : getModelKey();
|
||||
|
||||
if (!newModelKey) {
|
||||
new Notice("No model key found");
|
||||
throw new Error("No model key found");
|
||||
}
|
||||
|
||||
try {
|
||||
let customModel = findCustomModel(newModelKey, getSettings().activeModels);
|
||||
if (!customModel) {
|
||||
// Reset default model if no model is found
|
||||
console.error("Resetting default model. No model configuration found for: ", newModelKey);
|
||||
customModel = BUILTIN_CHAT_MODELS[0];
|
||||
newModelKey = customModel.name + "|" + customModel.provider;
|
||||
if (neededReInitChatMode) {
|
||||
let customModel = findCustomModel(newModelKey, getSettings().activeModels);
|
||||
if (!customModel) {
|
||||
// Reset default model if no model is found
|
||||
console.error("Resetting default model. No model configuration found for: ", newModelKey);
|
||||
customModel = BUILTIN_CHAT_MODELS[0];
|
||||
newModelKey = customModel.name + "|" + customModel.provider;
|
||||
}
|
||||
const mergedModel = {
|
||||
...customModel,
|
||||
...currentProject?.modelConfigs,
|
||||
};
|
||||
await this.chatModelManager.setChatModel(mergedModel);
|
||||
}
|
||||
await this.chatModelManager.setChatModel(customModel);
|
||||
|
||||
// Must update the chatModel for chain because ChainFactory always
|
||||
// retrieves the old chain without the chatModel change if it exists!
|
||||
// Create a new chain with the new chatModel
|
||||
this.setChain(getChainType());
|
||||
this.setChain(chainType, options);
|
||||
logInfo(`Setting model to ${newModelKey}`);
|
||||
} catch (error) {
|
||||
logError(`createChainWithNewModel failed: ${error}`);
|
||||
|
|
@ -143,7 +170,7 @@ export default class ChainManager {
|
|||
|
||||
switch (chainType) {
|
||||
case ChainType.LLM_CHAIN: {
|
||||
ChainManager.chain = ChainFactory.createNewLLMChain({
|
||||
this.chain = ChainFactory.createNewLLMChain({
|
||||
llm: chatModel,
|
||||
memory: memory,
|
||||
prompt: options.prompt || chatPrompt,
|
||||
|
|
@ -164,13 +191,13 @@ export default class ChainManager {
|
|||
});
|
||||
|
||||
// Create new conversational retrieval chain
|
||||
ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||
this.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||
{
|
||||
llm: chatModel,
|
||||
retriever: retriever,
|
||||
systemMessage: getSystemPrompt(),
|
||||
},
|
||||
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
||||
this.storeRetrieverDocuments.bind(this),
|
||||
getSettings().debug
|
||||
);
|
||||
|
||||
|
|
@ -185,7 +212,7 @@ export default class ChainManager {
|
|||
case ChainType.COPILOT_PLUS_CHAIN: {
|
||||
// For initial load of the plugin
|
||||
await this.initializeQAChain(options);
|
||||
ChainManager.chain = ChainFactory.createNewLLMChain({
|
||||
this.chain = ChainFactory.createNewLLMChain({
|
||||
llm: chatModel,
|
||||
memory: memory,
|
||||
prompt: options.prompt || chatPrompt,
|
||||
|
|
@ -196,6 +223,19 @@ export default class ChainManager {
|
|||
break;
|
||||
}
|
||||
|
||||
case ChainType.PROJECT_CHAIN: {
|
||||
// For initial load of the plugin
|
||||
await this.initializeQAChain(options);
|
||||
this.chain = ChainFactory.createNewLLMChain({
|
||||
llm: chatModel,
|
||||
memory: memory,
|
||||
prompt: options.prompt || chatPrompt,
|
||||
abortController: options.abortController,
|
||||
}) as RunnableSequence;
|
||||
setChainType(ChainType.PROJECT_CHAIN);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
this.validateChainType(chainType);
|
||||
break;
|
||||
|
|
@ -211,6 +251,8 @@ export default class ChainManager {
|
|||
return new VaultQAChainRunner(this);
|
||||
case ChainType.COPILOT_PLUS_CHAIN:
|
||||
return new CopilotPlusChainRunner(this);
|
||||
case ChainType.PROJECT_CHAIN:
|
||||
return new ProjectChainRunner(this);
|
||||
default:
|
||||
throw new Error(`Unsupported chain type: ${chainType}`);
|
||||
}
|
||||
|
|
@ -259,9 +301,10 @@ export default class ChainManager {
|
|||
]);
|
||||
}
|
||||
|
||||
this.setChain(getChainType(), {
|
||||
this.createChainWithNewModel({ prompt: effectivePrompt }, false);
|
||||
/*this.setChain(getChainType(), {
|
||||
prompt: effectivePrompt,
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
const chainRunner = this.getChainRunner();
|
||||
|
|
@ -286,4 +329,20 @@ export default class ChainManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clearHistory() {
|
||||
this.chatMessages = [];
|
||||
}
|
||||
|
||||
public getChatMessages(): ChatMessage[] {
|
||||
return this.chatMessages;
|
||||
}
|
||||
|
||||
public setChatMessages(messages: ChatMessage[]) {
|
||||
this.chatMessages = [...messages];
|
||||
}
|
||||
|
||||
public addChatMessage(message: ChatMessage) {
|
||||
this.chatMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|||
import { Notice } from "obsidian";
|
||||
import ChainManager from "./chainManager";
|
||||
import { COPILOT_TOOL_NAMES, IntentAnalyzer } from "./intentAnalyzer";
|
||||
import { getCurrentProject } from "@/aiParams";
|
||||
import ProjectManager from "./projectManager";
|
||||
|
||||
export interface ChainRunner {
|
||||
run(
|
||||
|
|
@ -164,7 +166,7 @@ class LLMChainRunner extends BaseChainRunner {
|
|||
let fullAIResponse = "";
|
||||
|
||||
try {
|
||||
const chain = ChainManager.getChain();
|
||||
const chain = this.chainManager.getChain();
|
||||
const chatStream = await chain.stream({
|
||||
input: userMessage.message,
|
||||
} as any);
|
||||
|
|
@ -221,7 +223,7 @@ class VaultQAChainRunner extends BaseChainRunner {
|
|||
const memory = this.chainManager.memoryManager.getMemory();
|
||||
const memoryVariables = await memory.loadMemoryVariables({});
|
||||
const chatHistory = extractChatHistory(memoryVariables);
|
||||
const qaStream = await ChainManager.getRetrievalChain().stream({
|
||||
const qaStream = await this.chainManager.getRetrievalChain().stream({
|
||||
question: userMessage.message,
|
||||
chat_history: chatHistory,
|
||||
} as any);
|
||||
|
|
@ -248,7 +250,7 @@ class VaultQAChainRunner extends BaseChainRunner {
|
|||
}
|
||||
|
||||
private addSourcestoResponse(response: string): string {
|
||||
const docTitles = extractUniqueTitlesFromDocs(ChainManager.retrievedDocuments);
|
||||
const docTitles = extractUniqueTitlesFromDocs(this.chainManager.getRetrievedDocuments());
|
||||
if (docTitles.length > 0) {
|
||||
const links = docTitles.map((title) => `- [[${title}]]`).join("\n");
|
||||
response += "\n\n#### Sources:\n\n" + links;
|
||||
|
|
@ -347,6 +349,10 @@ class CopilotPlusChainRunner extends BaseChainRunner {
|
|||
return this.hasCapability(model, ModelCapability.VISION);
|
||||
}
|
||||
|
||||
protected getSystemPrompt(): string {
|
||||
return getSystemPrompt();
|
||||
}
|
||||
|
||||
private async streamMultimodalResponse(
|
||||
textContent: string,
|
||||
userMessage: ChatMessage,
|
||||
|
|
@ -363,7 +369,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
|
|||
const messages: any[] = [];
|
||||
|
||||
// Add system message if available
|
||||
let fullSystemMessage = getSystemPrompt();
|
||||
let fullSystemMessage = this.getSystemPrompt();
|
||||
|
||||
// Add chat history context to system message if exists
|
||||
if (chatHistory.length > 0) {
|
||||
|
|
@ -693,4 +699,23 @@ class CopilotPlusChainRunner extends BaseChainRunner {
|
|||
}
|
||||
}
|
||||
|
||||
export { CopilotPlusChainRunner, LLMChainRunner, VaultQAChainRunner };
|
||||
class ProjectChainRunner extends CopilotPlusChainRunner {
|
||||
protected getSystemPrompt(): string {
|
||||
// get current project
|
||||
const projectConfig = getCurrentProject();
|
||||
|
||||
if (!projectConfig) {
|
||||
return super.getSystemPrompt();
|
||||
}
|
||||
|
||||
const context = ProjectManager.instance.getProjectContext(projectConfig.id);
|
||||
let finalPrompt = projectConfig.systemPrompt;
|
||||
|
||||
if (context) {
|
||||
finalPrompt += context;
|
||||
}
|
||||
return finalPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
export { CopilotPlusChainRunner, LLMChainRunner, VaultQAChainRunner, ProjectChainRunner };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CustomModel, getModelKey, ModelConfig, setModelKey } from "@/aiParams";
|
||||
import { CustomModel, getModelKey, ModelConfig } from "@/aiParams";
|
||||
import { BREVILABS_API_BASE_URL, BUILTIN_CHAT_MODELS, ChatModelProviders } from "@/constants";
|
||||
import { getDecryptedKey } from "@/encryptionService";
|
||||
import { logError } from "@/logger";
|
||||
|
|
@ -290,7 +290,8 @@ export default class ChatModelManager {
|
|||
|
||||
async setChatModel(model: CustomModel): Promise<void> {
|
||||
const modelKey = getModelKeyFromModel(model);
|
||||
setModelKey(modelKey);
|
||||
// todo 为啥这里需要重新设置?
|
||||
// setModelKey(modelKey);
|
||||
try {
|
||||
const modelInstance = await this.createModelInstance(model);
|
||||
ChatModelManager.chatModel = modelInstance;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { simpleYoutubeTranscriptionTool } from "@/tools/YoutubeTools";
|
|||
import { ToolManager } from "@/tools/toolManager";
|
||||
import { extractChatHistory, extractYoutubeUrl } from "@/utils";
|
||||
import { BrevilabsClient } from "./brevilabsClient";
|
||||
import MemoryManager from "./memoryManager";
|
||||
import { Vault } from "obsidian";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
// TODO: Add @index with explicit pdf files in chat context menu
|
||||
export const COPILOT_TOOL_NAMES = ["@vault", "@web", "@youtube", "@pomodoro"];
|
||||
|
|
@ -112,7 +112,7 @@ export class IntentAnalyzer {
|
|||
// Handle @web command
|
||||
if (message.includes("@web")) {
|
||||
const cleanQuery = this.removeAtCommands(originalMessage);
|
||||
const memory = MemoryManager.getInstance().getMemory();
|
||||
const memory = ProjectManager.instance.getCurrentChainManager().memoryManager.getMemory();
|
||||
const memoryVariables = await memory.loadMemoryVariables({});
|
||||
const chatHistory = extractChatHistory(memoryVariables);
|
||||
|
||||
|
|
|
|||
298
src/LLMProviders/projectManager.ts
Normal file
298
src/LLMProviders/projectManager.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import { App, Notice } from "obsidian";
|
||||
import ChainManager from "./chainManager";
|
||||
import VectorStoreManager from "../search/vectorStoreManager";
|
||||
import {
|
||||
getChainType,
|
||||
isProjectMode,
|
||||
ProjectConfig,
|
||||
setProjectLoading,
|
||||
subscribeToChainTypeChange,
|
||||
subscribeToModelKeyChange,
|
||||
subscribeToProjectChange,
|
||||
} from "@/aiParams";
|
||||
import { logError, logInfo } from "@/logger";
|
||||
import { ChainType } from "@/chainFactory";
|
||||
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
|
||||
import { err2String } from "@/utils";
|
||||
import { Mention } from "@/mentions/Mention";
|
||||
import { BrevilabsClient } from "./brevilabsClient";
|
||||
import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { CHAT_VIEWTYPE, VAULT_VECTOR_STORE_STRATEGY } from "@/constants";
|
||||
import CopilotView from "@/components/CopilotView";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { updateChatMemory } from "@/chatUtils";
|
||||
|
||||
export default class ProjectManager {
|
||||
public static instance: ProjectManager;
|
||||
private currentProjectId: string | null;
|
||||
private app: App;
|
||||
private plugin: CopilotPlugin;
|
||||
private readonly chainMangerInstance: ChainManager;
|
||||
|
||||
private projectContextCache: Map<string, string>;
|
||||
private chatMessageCache: Map<string, ChatMessage[]>;
|
||||
|
||||
private defaultProjectKey: string = "defaultProjectKey";
|
||||
|
||||
private constructor(app: App, vectorStoreManager: VectorStoreManager, plugin: CopilotPlugin) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.currentProjectId = null;
|
||||
|
||||
this.chainMangerInstance = new ChainManager(app, vectorStoreManager);
|
||||
this.projectContextCache = new Map();
|
||||
this.chatMessageCache = new Map();
|
||||
|
||||
// Set up subscriptions
|
||||
subscribeToModelKeyChange(async () => {
|
||||
await this.getCurrentChainManager().createChainWithNewModel();
|
||||
});
|
||||
|
||||
subscribeToChainTypeChange(async () => {
|
||||
// When switching from other modes to project mode, no need to update the chain.
|
||||
if (isProjectMode()) {
|
||||
return;
|
||||
}
|
||||
await this.getCurrentChainManager().createChainWithNewModel({
|
||||
refreshIndex:
|
||||
getSettings().indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH &&
|
||||
(getChainType() === ChainType.VAULT_QA_CHAIN ||
|
||||
getChainType() === ChainType.COPILOT_PLUS_CHAIN),
|
||||
});
|
||||
});
|
||||
|
||||
// Subscribe to Project changes
|
||||
subscribeToProjectChange(async (project) => {
|
||||
await this.switchProject(project);
|
||||
});
|
||||
|
||||
// Subscribe to settings changes to monitor projectList changes
|
||||
this.setupProjectListChangeMonitor();
|
||||
}
|
||||
|
||||
private setupProjectListChangeMonitor() {
|
||||
subscribeToSettingsChange(async (prev, next) => {
|
||||
if (!prev || !next) return;
|
||||
|
||||
const prevProjects = prev.projectList || [];
|
||||
const nextProjects = next.projectList || [];
|
||||
|
||||
// Find modified projects
|
||||
for (const nextProject of nextProjects) {
|
||||
const prevProject = prevProjects.find((p) => p.id === nextProject.id);
|
||||
if (prevProject) {
|
||||
// Check if project configuration has changed
|
||||
if (JSON.stringify(prevProject) !== JSON.stringify(nextProject)) {
|
||||
// Clear context cache for this project
|
||||
this.clearContextCache(nextProject.id);
|
||||
|
||||
// If this is the current project, reload its context
|
||||
if (this.currentProjectId === nextProject.id) {
|
||||
console.log("reload its context");
|
||||
await this.loadProjectContext(nextProject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static getInstance(
|
||||
app: App,
|
||||
vectorStoreManager: VectorStoreManager,
|
||||
plugin: CopilotPlugin
|
||||
): ProjectManager {
|
||||
if (!ProjectManager.instance) {
|
||||
ProjectManager.instance = new ProjectManager(app, vectorStoreManager, plugin);
|
||||
}
|
||||
return ProjectManager.instance;
|
||||
}
|
||||
|
||||
public getCurrentChainManager(): ChainManager {
|
||||
return this.chainMangerInstance;
|
||||
}
|
||||
|
||||
public async switchProject(project: ProjectConfig | null): Promise<void> {
|
||||
try {
|
||||
setProjectLoading(true);
|
||||
|
||||
// 1. save current project message. 2. load next project message
|
||||
|
||||
// switch default project
|
||||
if (!project) {
|
||||
await this.saveCurrentProjectMessage();
|
||||
this.currentProjectId = null; // ensure set currentProjectId
|
||||
|
||||
await this.loadNextProjectMessage();
|
||||
this.refreshChatView();
|
||||
return;
|
||||
}
|
||||
|
||||
// else
|
||||
const projectId = project.id;
|
||||
if (this.currentProjectId === projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveCurrentProjectMessage();
|
||||
this.currentProjectId = projectId; // ensure set currentProjectId
|
||||
|
||||
await Promise.all([
|
||||
// load memory
|
||||
this.loadNextProjectMessage(),
|
||||
// update chat model
|
||||
this.getCurrentChainManager().createChainWithNewModel(),
|
||||
// load context
|
||||
this.loadProjectContext(project),
|
||||
]);
|
||||
|
||||
// fresh chat view
|
||||
this.refreshChatView();
|
||||
|
||||
logInfo(`Switched to project: ${project.name}`);
|
||||
} catch (error) {
|
||||
logError(`Failed to switch project: ${error}`);
|
||||
throw error;
|
||||
} finally {
|
||||
setProjectLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCurrentProjectMessage() {
|
||||
// save show message
|
||||
this.chatMessageCache.set(
|
||||
this.currentProjectId ? this.currentProjectId : this.defaultProjectKey,
|
||||
this.getCurrentChainManager().getChatMessages()
|
||||
);
|
||||
|
||||
// todo 需不需要?
|
||||
await this.plugin.autosaveCurrentChat();
|
||||
}
|
||||
|
||||
private async loadNextProjectMessage() {
|
||||
const chainManager = this.getCurrentChainManager();
|
||||
|
||||
const messages =
|
||||
this.chatMessageCache.get(
|
||||
this.currentProjectId ? this.currentProjectId : this.defaultProjectKey
|
||||
) ?? [];
|
||||
|
||||
chainManager.setChatMessages(messages);
|
||||
await updateChatMemory(messages, chainManager.memoryManager);
|
||||
}
|
||||
|
||||
private async loadProjectContext(project: ProjectConfig): Promise<void> {
|
||||
try {
|
||||
if (project.contextSource) {
|
||||
if (this.projectContextCache.has(project.id)) {
|
||||
logInfo(`Using cached context for project: ${project.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contextParts = await Promise.all([
|
||||
this.processMarkdownContext(
|
||||
project.contextSource.inclusions,
|
||||
project.contextSource.exclusions
|
||||
),
|
||||
this.processWebUrlsContext(project.contextSource.webUrls),
|
||||
this.processYoutubeUrlsContext(project.contextSource.youtubeUrls),
|
||||
]).then((res) => res.filter((it) => !!it));
|
||||
|
||||
const contextText = `
|
||||
|
||||
<ProjectContext>
|
||||
# Project Context
|
||||
The following information is relevant context for this project. Use this information to inform your responses when appropriate:
|
||||
|
||||
${contextParts.join("\n")}
|
||||
<ProjectContext>
|
||||
`;
|
||||
|
||||
this.projectContextCache.set(project.id, contextText);
|
||||
}
|
||||
} catch (error) {
|
||||
logError(`Failed to load project context: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private refreshChatView() {
|
||||
// get chat view
|
||||
const chatView = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0]?.view as CopilotView;
|
||||
if (chatView) {
|
||||
chatView.updateView();
|
||||
}
|
||||
}
|
||||
|
||||
public getProjectContext(projectId: string): string | null {
|
||||
return this.projectContextCache.get(projectId) || null;
|
||||
}
|
||||
|
||||
public clearContextCache(projectId: string): void {
|
||||
this.projectContextCache.delete(projectId);
|
||||
logInfo(`Context cache cleared for project: ${projectId}`);
|
||||
}
|
||||
|
||||
private async processMarkdownContext(inclusions?: string, exclusions?: string): Promise<string> {
|
||||
if (!inclusions && !exclusions) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let allContent = "";
|
||||
const { inclusions: inclusionPatterns, exclusions: exclusionPatterns } = getMatchingPatterns({
|
||||
inclusions,
|
||||
exclusions,
|
||||
});
|
||||
|
||||
const files = this.app.vault.getMarkdownFiles().filter((file) => {
|
||||
return shouldIndexFile(file, inclusionPatterns, exclusionPatterns);
|
||||
});
|
||||
|
||||
await Promise.all(files.map((file) => this.app.vault.cachedRead(file))).then((contents) =>
|
||||
contents.map((c) => (allContent += c + " "))
|
||||
);
|
||||
|
||||
return allContent;
|
||||
}
|
||||
|
||||
private async processWebUrlsContext(webUrls?: string): Promise<string> {
|
||||
if (!webUrls?.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const mention = Mention.getInstance();
|
||||
const { urlContext } = await mention.processUrls(webUrls);
|
||||
return urlContext || "";
|
||||
} catch (error) {
|
||||
logError(`Failed to process web URLs: ${error}`);
|
||||
new Notice(`Failed to process web URLs: ${err2String(error)}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private async processYoutubeUrlsContext(youtubeUrls?: string): Promise<string> {
|
||||
if (!youtubeUrls?.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const urls = youtubeUrls.split("\n").filter((url) => url.trim());
|
||||
const processPromises = urls.map(async (url) => {
|
||||
try {
|
||||
const response = await BrevilabsClient.getInstance().youtube4llm(url);
|
||||
if (response.response.transcript) {
|
||||
return `\n\nYouTube transcript from ${url}:\n${response.response.transcript}`;
|
||||
}
|
||||
return "";
|
||||
} catch (error) {
|
||||
logError(`Failed to process YouTube URL ${url}: ${error}`);
|
||||
new Notice(`Failed to process YouTube URL ${url}: ${err2String(error)}`);
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(processPromises);
|
||||
return results.join("");
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
MessagesPlaceholder,
|
||||
SystemMessagePromptTemplate,
|
||||
} from "@langchain/core/prompts";
|
||||
import { getCurrentProject, isProjectMode, subscribeToProjectChange } from "@/aiParams";
|
||||
|
||||
export default class PromptManager {
|
||||
private static instance: PromptManager;
|
||||
|
|
@ -19,6 +20,11 @@ export default class PromptManager {
|
|||
this.initChatPrompt();
|
||||
this.initQAPrompt();
|
||||
});
|
||||
|
||||
subscribeToProjectChange(() => {
|
||||
this.initChatPrompt();
|
||||
this.initQAPrompt();
|
||||
});
|
||||
}
|
||||
|
||||
static getInstance(): PromptManager {
|
||||
|
|
@ -29,8 +35,15 @@ export default class PromptManager {
|
|||
}
|
||||
|
||||
private initChatPrompt(): void {
|
||||
let systemPrompt = getSystemPrompt();
|
||||
|
||||
const currentProject = getCurrentProject();
|
||||
if (currentProject && isProjectMode()) {
|
||||
systemPrompt = currentProject.systemPrompt;
|
||||
}
|
||||
|
||||
// Escape curly braces in the system message
|
||||
const escapedSystemMessage = this.escapeTemplateString(getSystemPrompt());
|
||||
const escapedSystemMessage = this.escapeTemplateString(systemPrompt);
|
||||
|
||||
this.chatPrompt = ChatPromptTemplate.fromMessages([
|
||||
SystemMessagePromptTemplate.fromTemplate(escapedSystemMessage),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,29 @@ const chainTypeAtom = atom(
|
|||
}
|
||||
);
|
||||
|
||||
const currentProjectAtom = atom<ProjectConfig | null>(null);
|
||||
const projectLoadingAtom = atom<boolean>(false);
|
||||
|
||||
export interface ProjectConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
systemPrompt: string;
|
||||
projectModelKey: string;
|
||||
modelConfigs: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
};
|
||||
contextSource: {
|
||||
inclusions: string;
|
||||
exclusions?: string;
|
||||
webUrls?: string;
|
||||
youtubeUrls?: string;
|
||||
};
|
||||
created: number;
|
||||
UsageTimestamps: number;
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
modelName: string;
|
||||
temperature: number;
|
||||
|
|
@ -79,6 +102,7 @@ export interface CustomModel {
|
|||
stream?: boolean;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
|
||||
context?: number;
|
||||
plusExclusive?: boolean;
|
||||
believerExclusive?: boolean;
|
||||
|
|
@ -132,3 +156,49 @@ export function useChainType() {
|
|||
store: settingsStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function setCurrentProject(project: ProjectConfig | null) {
|
||||
settingsStore.set(currentProjectAtom, project);
|
||||
}
|
||||
|
||||
export function getCurrentProject(): ProjectConfig | null {
|
||||
return settingsStore.get(currentProjectAtom);
|
||||
}
|
||||
|
||||
export function subscribeToProjectChange(
|
||||
callback: (project: ProjectConfig | null) => void
|
||||
): () => void {
|
||||
return settingsStore.sub(currentProjectAtom, () => {
|
||||
callback(settingsStore.get(currentProjectAtom));
|
||||
});
|
||||
}
|
||||
|
||||
export function useCurrentProject() {
|
||||
return useAtom(currentProjectAtom, {
|
||||
store: settingsStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function setProjectLoading(loading: boolean) {
|
||||
settingsStore.set(projectLoadingAtom, loading);
|
||||
}
|
||||
|
||||
export function isProjectLoading(): boolean {
|
||||
return settingsStore.get(projectLoadingAtom);
|
||||
}
|
||||
|
||||
export function subscribeToProjectLoadingChange(callback: (loading: boolean) => void): () => void {
|
||||
return settingsStore.sub(projectLoadingAtom, () => {
|
||||
callback(settingsStore.get(projectLoadingAtom));
|
||||
});
|
||||
}
|
||||
|
||||
export function useProjectLoading() {
|
||||
return useAtom(projectLoadingAtom, {
|
||||
store: settingsStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function isProjectMode() {
|
||||
return getChainType() === ChainType.PROJECT_CHAIN;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export enum ChainType {
|
|||
LLM_CHAIN = "llm_chain",
|
||||
VAULT_QA_CHAIN = "vault_qa",
|
||||
COPILOT_PLUS_CHAIN = "copilot_plus",
|
||||
PROJECT_CHAIN = "project",
|
||||
}
|
||||
|
||||
class ChainFactory {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import { removeThinkTags } from "@/utils";
|
||||
import { BaseChatModelCallOptions } from "@langchain/core/language_models/chat_models";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
export async function getStandaloneQuestion(
|
||||
question: string,
|
||||
|
|
@ -22,8 +22,9 @@ export async function getStandaloneQuestion(
|
|||
.map(([human, ai]) => `Human: ${human}\nAssistant: ${ai}`)
|
||||
.join("\n");
|
||||
|
||||
const chatModel = ChatModelManager.getInstance()
|
||||
.getChatModel()
|
||||
const chatModel = ProjectManager.instance
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.getChatModel()
|
||||
.bind({ temperature: 0 } as BaseChatModelCallOptions);
|
||||
const response = await chatModel.invoke([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -124,14 +124,18 @@ export function registerCommands(
|
|||
addEditorCommand(plugin, COMMAND_IDS.COUNT_WORD_AND_TOKENS_SELECTION, async (editor: Editor) => {
|
||||
const selectedText = await editor.getSelection();
|
||||
const wordCount = selectedText.split(" ").length;
|
||||
const tokenCount = await plugin.chainManager.chatModelManager.countTokens(selectedText);
|
||||
const tokenCount = await plugin.projectManager
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.countTokens(selectedText);
|
||||
new Notice(`Selected text contains ${wordCount} words and ${tokenCount} tokens.`);
|
||||
});
|
||||
|
||||
addCommand(plugin, COMMAND_IDS.COUNT_TOTAL_VAULT_TOKENS, async () => {
|
||||
try {
|
||||
const allContent = await getAllQAMarkdownContent(plugin.app);
|
||||
const totalTokens = await plugin.chainManager.chatModelManager.countTokens(allContent);
|
||||
const totalTokens = await plugin.projectManager
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.countTokens(allContent);
|
||||
new Notice(`Total tokens in your vault: ${totalTokens}`);
|
||||
} catch (error) {
|
||||
console.error("Error counting tokens: ", error);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useChainType, useModelKey } from "@/aiParams";
|
||||
import { ProjectConfig, useChainType, useModelKey } from "@/aiParams";
|
||||
import { ChainType } from "@/chainFactory";
|
||||
import { updateChatMemory } from "@/chatUtils";
|
||||
import { ChatControls } from "@/components/chat-components/ChatControls";
|
||||
import ChatInput from "@/components/chat-components/ChatInput";
|
||||
import ChatMessages from "@/components/chat-components/ChatMessages";
|
||||
import { ProjectList } from "@/components/chat-components/ProjectList";
|
||||
import { ABORT_REASON, COMMAND_IDS, EVENT_NAMES, LOADING_MESSAGES, USER_SENDER } from "@/constants";
|
||||
import { AppContext, EventTargetContext } from "@/context";
|
||||
import { ContextProcessor } from "@/contextProcessor";
|
||||
|
|
@ -12,7 +13,7 @@ import { getAIResponse } from "@/langchainStream";
|
|||
import ChainManager from "@/LLMProviders/chainManager";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { Mention } from "@/mentions/Mention";
|
||||
import { getSettings, useSettingsValue } from "@/settings/model";
|
||||
import { getSettings, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import SharedState, { ChatMessage, useSharedState } from "@/sharedState";
|
||||
import { FileParserManager } from "@/tools/FileParserManager";
|
||||
import { err2String, formatDateTime } from "@/utils";
|
||||
|
|
@ -20,6 +21,8 @@ import { Notice, TFile } from "obsidian";
|
|||
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
type ChatMode = "default" | "project";
|
||||
|
||||
interface ChatProps {
|
||||
sharedState: SharedState;
|
||||
chainManager: ChainManager;
|
||||
|
|
@ -27,6 +30,7 @@ interface ChatProps {
|
|||
updateUserMessageHistory: (newMessage: string) => void;
|
||||
fileParserManager: FileParserManager;
|
||||
plugin: CopilotPlugin;
|
||||
mode?: ChatMode;
|
||||
}
|
||||
|
||||
const Chat: React.FC<ChatProps> = ({
|
||||
|
|
@ -51,6 +55,10 @@ const Chat: React.FC<ChatProps> = ({
|
|||
const [contextNotes, setContextNotes] = useState<TFile[]>([]);
|
||||
const [includeActiveNote, setIncludeActiveNote] = useState(false);
|
||||
const [selectedImages, setSelectedImages] = useState<File[]>([]);
|
||||
const [showChatUI, setShowChatUI] = useState(false);
|
||||
|
||||
const [previousMode, setPreviousMode] = useState<ChainType | null>(null);
|
||||
const [mode, setSelectedChain] = useChainType();
|
||||
|
||||
const mention = Mention.getInstance();
|
||||
|
||||
|
|
@ -533,6 +541,40 @@ ${chatContent}`;
|
|||
[addMessage, chainManager.memoryManager, chatHistory, clearMessages]
|
||||
);
|
||||
|
||||
const handleAddProject = useCallback(
|
||||
(project: ProjectConfig) => {
|
||||
const currentProjects = settings.projectList || [];
|
||||
const existingIndex = currentProjects.findIndex((p) => p.name === project.name);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
throw new Error(`Project "${project.name}" already exists, please use a different name`);
|
||||
}
|
||||
|
||||
const newProjectList = [...currentProjects, project];
|
||||
updateSetting("projectList", newProjectList);
|
||||
new Notice(`${project.name} added successfully`);
|
||||
return true;
|
||||
},
|
||||
[settings.projectList]
|
||||
);
|
||||
|
||||
const handleEditProject = useCallback(
|
||||
(originP: ProjectConfig, updateP: ProjectConfig) => {
|
||||
const currentProjects = settings.projectList || [];
|
||||
const existingProject = currentProjects.find((p) => p.name === originP.name);
|
||||
|
||||
if (!existingProject) {
|
||||
throw new Error(`Project "${originP.name}" does not exist`);
|
||||
}
|
||||
|
||||
const newProjectList = currentProjects.map((p) => (p.name === originP.name ? updateP : p));
|
||||
updateSetting("projectList", newProjectList);
|
||||
new Notice(`${originP.name} updated successfully`);
|
||||
return true;
|
||||
},
|
||||
[settings.projectList]
|
||||
);
|
||||
|
||||
const handleInsertToChat = useCallback((prompt: string) => {
|
||||
setInputMessage((prev) => `${prev} ${prompt} `);
|
||||
}, []);
|
||||
|
|
@ -556,22 +598,31 @@ ${chatContent}`;
|
|||
handleSaveAsNote,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<ChatMessages
|
||||
chatHistory={chatHistory}
|
||||
currentAiMessage={currentAiMessage}
|
||||
loading={loading}
|
||||
loadingMessage={loadingMessage}
|
||||
app={app}
|
||||
onRegenerate={handleRegenerate}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onInsertToChat={handleInsertToChat}
|
||||
onReplaceChat={setInputMessage}
|
||||
/>
|
||||
<div className="bottom-container">
|
||||
<ChatControls onNewChat={handleNewChat} onSaveAsNote={() => handleSaveAsNote(true)} />
|
||||
const renderChatComponents = () => (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ChatMessages
|
||||
chatHistory={chatHistory}
|
||||
currentAiMessage={currentAiMessage}
|
||||
loading={loading}
|
||||
loadingMessage={loadingMessage}
|
||||
app={app}
|
||||
onRegenerate={handleRegenerate}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onInsertToChat={handleInsertToChat}
|
||||
onReplaceChat={setInputMessage}
|
||||
showHelperComponents={mode !== "project"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-end w-full flex-none box-border sticky bottom-0 border-t border-border">
|
||||
<ChatControls
|
||||
onNewChat={handleNewChat}
|
||||
onSaveAsNote={() => handleSaveAsNote(true)}
|
||||
onModeChange={(newMode) => {
|
||||
setPreviousMode(mode);
|
||||
}}
|
||||
/>
|
||||
<ChatInput
|
||||
ref={inputRef}
|
||||
inputMessage={inputMessage}
|
||||
|
|
@ -589,8 +640,44 @@ ${chatContent}`;
|
|||
selectedImages={selectedImages}
|
||||
onAddImage={(files: File[]) => setSelectedImages((prev) => [...prev, ...files])}
|
||||
setSelectedImages={setSelectedImages}
|
||||
disableModelSwitch={mode === "project"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<div className="h-full">
|
||||
<div className="flex flex-col h-full relative">
|
||||
{mode === ChainType.PROJECT_CHAIN && (
|
||||
<div className={`${mode === ChainType.PROJECT_CHAIN ? "z-modal" : ""}`}>
|
||||
<ProjectList
|
||||
projects={settings.projectList || []}
|
||||
defaultOpen={true}
|
||||
app={app}
|
||||
hasMessages={false}
|
||||
onProjectAdded={handleAddProject}
|
||||
onEditProject={handleEditProject}
|
||||
inputRef={inputRef}
|
||||
onClose={() => {
|
||||
if (previousMode) {
|
||||
setSelectedChain(previousMode);
|
||||
setPreviousMode(null);
|
||||
} else {
|
||||
// default back to chat or plus mode
|
||||
setSelectedChain(
|
||||
settings.isPlusUser ? ChainType.COPILOT_PLUS_CHAIN : ChainType.LLM_CHAIN
|
||||
);
|
||||
}
|
||||
}}
|
||||
showChatUI={(v) => setShowChatUI(v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(mode !== "project" || (mode === "project" && showChatUI)) && renderChatComponents()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ import { FileParserManager } from "@/tools/FileParserManager";
|
|||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { Root, createRoot } from "react-dom/client";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
|
||||
export default class CopilotView extends ItemView {
|
||||
private chainManager: ChainManager;
|
||||
private get chainManager(): ChainManager {
|
||||
return this.plugin.projectManager.getCurrentChainManager();
|
||||
}
|
||||
|
||||
private fileParserManager: FileParserManager;
|
||||
private root: Root | null = null;
|
||||
private handleSaveAsNote: (() => Promise<void>) | null = null;
|
||||
|
|
@ -25,7 +28,6 @@ export default class CopilotView extends ItemView {
|
|||
super(leaf);
|
||||
this.sharedState = plugin.sharedState;
|
||||
this.app = plugin.app;
|
||||
this.chainManager = plugin.chainManager;
|
||||
this.fileParserManager = plugin.fileParserManager;
|
||||
this.eventTarget = new EventTarget();
|
||||
this.plugin = plugin;
|
||||
|
|
@ -50,14 +52,24 @@ export default class CopilotView extends ItemView {
|
|||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const root = createRoot(this.containerEl.children[1]);
|
||||
this.root = createRoot(this.containerEl.children[1]);
|
||||
const handleSaveAsNote = (saveFunction: () => Promise<void>) => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
const updateUserMessageHistory = (newMessage: string) => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
root.render(
|
||||
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
}
|
||||
|
||||
private renderView(
|
||||
handleSaveAsNote: (saveFunction: () => Promise<void>) => void,
|
||||
updateUserMessageHistory: (newMessage: string) => void
|
||||
): void {
|
||||
if (!this.root) return;
|
||||
|
||||
this.root.render(
|
||||
<AppContext.Provider value={this.app}>
|
||||
<EventTargetContext.Provider value={this.eventTarget}>
|
||||
<React.StrictMode>
|
||||
|
|
@ -83,14 +95,26 @@ export default class CopilotView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
updateView(): void {
|
||||
// load currentChainManager chatMessages
|
||||
this.sharedState.replaceMessages(
|
||||
this.plugin.projectManager.getCurrentChainManager().getChatMessages()
|
||||
);
|
||||
|
||||
const handleSaveAsNote = (saveFunction: () => Promise<void>) => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
const updateUserMessageHistory = (newMessage: string) => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateView(): void {
|
||||
// Force a re-render of the React component
|
||||
this.onOpen();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Download,
|
||||
MoreHorizontal,
|
||||
Sparkles,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
MessageCirclePlus,
|
||||
ChevronDown,
|
||||
Download,
|
||||
FileText,
|
||||
LibraryBig,
|
||||
MessageCirclePlus,
|
||||
MoreHorizontal,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
SquareArrowOutUpRight,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -15,7 +16,7 @@ import { DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-
|
|||
import { SettingSwitch } from "@/components/ui/setting-switch";
|
||||
import { updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useChainType } from "@/aiParams";
|
||||
import { setCurrentProject, useChainType } from "@/aiParams";
|
||||
import { ChainType } from "@/chainFactory";
|
||||
import { Notice } from "obsidian";
|
||||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
|
|
@ -35,13 +36,29 @@ export async function refreshVaultIndex() {
|
|||
interface ChatControlsProps {
|
||||
onNewChat: () => void;
|
||||
onSaveAsNote: () => void;
|
||||
onModeChange: (mode: ChainType) => void;
|
||||
onCloseProject?: () => void;
|
||||
}
|
||||
|
||||
export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
||||
export function ChatControls({
|
||||
onNewChat,
|
||||
onSaveAsNote,
|
||||
onModeChange,
|
||||
onCloseProject,
|
||||
}: ChatControlsProps) {
|
||||
const settings = useSettingsValue();
|
||||
const [selectedChain, setSelectedChain] = useChainType();
|
||||
const isPlusUser = useIsPlusUser();
|
||||
|
||||
const handleModeChange = (chainType: ChainType) => {
|
||||
setSelectedChain(chainType);
|
||||
onModeChange(chainType);
|
||||
if (chainType !== ChainType.PROJECT_CHAIN) {
|
||||
setCurrentProject(null);
|
||||
onCloseProject?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full py-1 flex justify-between items-center px-1">
|
||||
<div className="flex-1">
|
||||
|
|
@ -56,18 +73,31 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
|||
copilot plus (beta)
|
||||
</div>
|
||||
)}
|
||||
{selectedChain === ChainType.PROJECT_CHAIN && "plus projects (alpha)"}
|
||||
<ChevronDown className="size-5 mt-0.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.LLM_CHAIN)}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.LLM_CHAIN);
|
||||
}}
|
||||
>
|
||||
chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.VAULT_QA_CHAIN)}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.VAULT_QA_CHAIN);
|
||||
}}
|
||||
>
|
||||
vault QA
|
||||
</DropdownMenuItem>
|
||||
{isPlusUser ? (
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.COPILOT_PLUS_CHAIN);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Sparkles className="size-4" />
|
||||
copilot plus (beta)
|
||||
|
|
@ -77,6 +107,29 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
|||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT);
|
||||
onCloseProject?.();
|
||||
}}
|
||||
>
|
||||
copilot plus (beta)
|
||||
<SquareArrowOutUpRight className="size-3" />
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{isPlusUser ? (
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-1"
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.PROJECT_CHAIN);
|
||||
}}
|
||||
>
|
||||
<LibraryBig className="size-4" />
|
||||
plus projects (alpha)
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT);
|
||||
onCloseProject?.();
|
||||
}}
|
||||
>
|
||||
copilot plus (beta)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import { useChainType, useModelKey } from "@/aiParams";
|
||||
import {
|
||||
useChainType,
|
||||
useModelKey,
|
||||
useProjectLoading,
|
||||
ProjectConfig,
|
||||
getCurrentProject,
|
||||
subscribeToProjectChange,
|
||||
} from "@/aiParams";
|
||||
import { ChainType } from "@/chainFactory";
|
||||
import { AddContextNoteModal } from "@/components/modals/AddContextNoteModal";
|
||||
import { AddImageModal } from "@/components/modals/AddImageModal";
|
||||
|
|
@ -24,9 +31,9 @@ import {
|
|||
Command,
|
||||
CornerDownLeft,
|
||||
Image,
|
||||
Loader2,
|
||||
StopCircle,
|
||||
X,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { App, Notice, Platform, TFile } from "obsidian";
|
||||
import React, {
|
||||
|
|
@ -61,6 +68,7 @@ interface ChatInputProps {
|
|||
selectedImages: File[];
|
||||
onAddImage: (files: File[]) => void;
|
||||
setSelectedImages: React.Dispatch<React.SetStateAction<File[]>>;
|
||||
disableModelSwitch?: boolean;
|
||||
}
|
||||
|
||||
const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
||||
|
|
@ -81,6 +89,7 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
selectedImages,
|
||||
onAddImage,
|
||||
setSelectedImages,
|
||||
disableModelSwitch,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
|
|
@ -93,11 +102,14 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
const [currentModelKey, setCurrentModelKey] = useModelKey();
|
||||
const [modelError, setModelError] = useState<string | null>(null);
|
||||
const [currentChain] = useChainType();
|
||||
const [isProjectLoading] = useProjectLoading();
|
||||
const [currentActiveNote, setCurrentActiveNote] = useState<TFile | null>(
|
||||
app.workspace.getActiveFile()
|
||||
);
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectConfig | null>(null);
|
||||
const settings = useSettingsValue();
|
||||
const isCopilotPlus = currentChain === ChainType.COPILOT_PLUS_CHAIN;
|
||||
const isCopilotPlus =
|
||||
currentChain === ChainType.COPILOT_PLUS_CHAIN || currentChain === ChainType.PROJECT_CHAIN;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
|
|
@ -105,6 +117,33 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (currentChain === ChainType.PROJECT_CHAIN) {
|
||||
setSelectedProject(getCurrentProject());
|
||||
|
||||
const unsubscribe = subscribeToProjectChange((project) => {
|
||||
setSelectedProject(project);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
} else {
|
||||
setSelectedProject(null);
|
||||
}
|
||||
}, [currentChain]);
|
||||
|
||||
const getDisplayModelKey = (): string => {
|
||||
if (
|
||||
selectedProject &&
|
||||
currentChain === ChainType.PROJECT_CHAIN &&
|
||||
selectedProject.projectModelKey
|
||||
) {
|
||||
return selectedProject.projectModelKey;
|
||||
}
|
||||
return currentModelKey;
|
||||
};
|
||||
|
||||
const onSendMessage = (includeVault: boolean) => {
|
||||
if (!isCopilotPlus) {
|
||||
handleSendMessage();
|
||||
|
|
@ -458,6 +497,14 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
)}
|
||||
|
||||
<div className="relative" {...(isCopilotPlus ? getRootProps() : {})}>
|
||||
{isProjectLoading && (
|
||||
<div className="absolute inset-0 z-modal bg-background/80 backdrop-blur-sm flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span className="text-sm">Loading the project context...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
className="w-full bg-transparent focus-visible:ring-0 border-none min-h-[60px] max-h-40 overflow-y-auto resize-none px-2 rounded-md text-sm text-normal"
|
||||
|
|
@ -469,6 +516,7 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
disabled={isProjectLoading}
|
||||
/>
|
||||
{isCopilotPlus && (
|
||||
<>
|
||||
|
|
@ -492,17 +540,18 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
) : (
|
||||
<DropdownMenu open={isModelDropdownOpen} onOpenChange={setIsModelDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost2" size="fit">
|
||||
<Button variant="ghost2" size="fit" disabled={disableModelSwitch}>
|
||||
{modelError ? (
|
||||
<span className="text-error">Model Load Failed</span>
|
||||
) : settings.activeModels.find(
|
||||
(model) => model.enabled && getModelKeyFromModel(model) === currentModelKey
|
||||
(model) =>
|
||||
model.enabled && getModelKeyFromModel(model) === getDisplayModelKey()
|
||||
) ? (
|
||||
<ModelDisplay
|
||||
model={
|
||||
settings.activeModels.find(
|
||||
(model) =>
|
||||
model.enabled && getModelKeyFromModel(model) === currentModelKey
|
||||
model.enabled && getModelKeyFromModel(model) === getDisplayModelKey()
|
||||
)!
|
||||
}
|
||||
iconSize={8}
|
||||
|
|
@ -510,47 +559,48 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
) : (
|
||||
"Select Model"
|
||||
)}
|
||||
<ChevronDown className="size-5 mt-0.5" />
|
||||
{!disableModelSwitch && <ChevronDown className="size-5 mt-0.5" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start">
|
||||
{settings.activeModels
|
||||
.filter((model) => model.enabled)
|
||||
.map((model) => {
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(model, settings);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getModelKeyFromModel(model)}
|
||||
onSelect={async (event) => {
|
||||
if (!hasApiKey && errorNotice) {
|
||||
event.preventDefault();
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setModelError(null);
|
||||
setCurrentModelKey(getModelKeyFromModel(model));
|
||||
} catch (error) {
|
||||
const msg = `Model switch failed: ` + err2String(error);
|
||||
setModelError(msg);
|
||||
new Notice(msg);
|
||||
// Restore to the last valid model
|
||||
const lastValidModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === currentModelKey
|
||||
);
|
||||
if (lastValidModel) {
|
||||
setCurrentModelKey(getModelKeyFromModel(lastValidModel));
|
||||
{!disableModelSwitch &&
|
||||
settings.activeModels
|
||||
.filter((model) => model.enabled)
|
||||
.map((model) => {
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(model, settings);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getModelKeyFromModel(model)}
|
||||
onSelect={async (event) => {
|
||||
if (!hasApiKey && errorNotice) {
|
||||
event.preventDefault();
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={!hasApiKey ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
<ModelDisplay model={model} iconSize={12} />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
try {
|
||||
setModelError(null);
|
||||
setCurrentModelKey(getModelKeyFromModel(model));
|
||||
} catch (error) {
|
||||
const msg = `Model switch failed: ` + err2String(error);
|
||||
setModelError(msg);
|
||||
new Notice(msg);
|
||||
// Restore to the last valid model
|
||||
const lastValidModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === currentModelKey
|
||||
);
|
||||
if (lastValidModel) {
|
||||
setCurrentModelKey(getModelKeyFromModel(lastValidModel));
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={!hasApiKey ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
<ModelDisplay model={model} iconSize={12} />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface ChatMessagesProps {
|
|||
onDelete: (messageIndex: number) => void;
|
||||
onInsertToChat: (prompt: string) => void;
|
||||
onReplaceChat: (prompt: string) => void;
|
||||
showHelperComponents: boolean;
|
||||
}
|
||||
|
||||
const ChatMessages = memo(
|
||||
|
|
@ -31,6 +32,7 @@ const ChatMessages = memo(
|
|||
onDelete,
|
||||
onInsertToChat,
|
||||
onReplaceChat,
|
||||
showHelperComponents = true,
|
||||
}: ChatMessagesProps) => {
|
||||
const [loadingDots, setLoadingDots] = useState("");
|
||||
|
||||
|
|
@ -63,14 +65,16 @@ const ChatMessages = memo(
|
|||
if (!chatHistory.filter((message) => message.isVisible).length && !currentAiMessage) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 overflow-y-auto w-full h-full">
|
||||
{settings.showRelevantNotes && (
|
||||
{showHelperComponents && settings.showRelevantNotes && (
|
||||
<RelevantNotes
|
||||
onInsertToChat={onInsertToChat}
|
||||
defaultOpen={true}
|
||||
key="relevant-notes-before-chat"
|
||||
/>
|
||||
)}
|
||||
{settings.showSuggestedPrompts && <SuggestedPrompts onClick={onReplaceChat} />}
|
||||
{showHelperComponents && settings.showSuggestedPrompts && (
|
||||
<SuggestedPrompts onClick={onReplaceChat} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -81,7 +85,7 @@ const ChatMessages = memo(
|
|||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
{settings.showRelevantNotes && (
|
||||
{showHelperComponents && settings.showRelevantNotes && (
|
||||
<RelevantNotes
|
||||
className="mb-4"
|
||||
onInsertToChat={onInsertToChat}
|
||||
|
|
|
|||
369
src/components/chat-components/ProjectList.tsx
Normal file
369
src/components/chat-components/ProjectList.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ProjectConfig, setCurrentProject } from "@/aiParams";
|
||||
import {
|
||||
BookOpen,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Edit2,
|
||||
Folder,
|
||||
Info,
|
||||
MessageSquare,
|
||||
Plus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import React, { memo, useEffect, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { AddProjectModal } from "@/components/modals/AddProjectModal";
|
||||
import { App } from "obsidian";
|
||||
import { logError } from "@/logger";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
function ProjectItem({
|
||||
project,
|
||||
loadContext,
|
||||
onEdit,
|
||||
}: {
|
||||
project: ProjectConfig;
|
||||
loadContext: (project: ProjectConfig) => void;
|
||||
onEdit: (project: ProjectConfig) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 p-3 justify-between items-center rounded-lg bg-secondary/40 border border-border border-solid group transition-all duration-200 hover:border-interactive-accent/30 hover:bg-interactive-accent/5 hover:shadow-[0_2px_12px_rgba(0,0,0,0.1)] active:scale-[0.98] cursor-pointer"
|
||||
onDoubleClick={() => loadContext(project)}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 overflow-hidden">
|
||||
<div className="text-blue-400">
|
||||
<Folder className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 flex-1 overflow-hidden">
|
||||
<span className="text-[13px] font-medium text-normal text-ellipsis overflow-hidden whitespace-nowrap w-full">
|
||||
{project.name}
|
||||
</span>
|
||||
{project.description && (
|
||||
<span className="text-[12px] text-muted/80 text-ellipsis overflow-hidden whitespace-nowrap w-full">
|
||||
{project.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 opacity-100 transition-opacity duration-200">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-accent/10 hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(project);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Edit Project</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-accent/10 hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
loadContext(project);
|
||||
}}
|
||||
>
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Start Chat</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ProjectList = memo(
|
||||
({
|
||||
className,
|
||||
projects,
|
||||
defaultOpen = false,
|
||||
app,
|
||||
onProjectAdded,
|
||||
onEditProject,
|
||||
hasMessages = false,
|
||||
showChatUI,
|
||||
onClose,
|
||||
inputRef,
|
||||
}: {
|
||||
className?: string;
|
||||
projects: ProjectConfig[];
|
||||
defaultOpen?: boolean;
|
||||
app: App;
|
||||
onProjectAdded: (project: ProjectConfig) => void;
|
||||
onEditProject: (originP: ProjectConfig, updateP: ProjectConfig) => void;
|
||||
hasMessages?: boolean;
|
||||
showChatUI: (v: boolean) => void;
|
||||
onClose: () => void;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}): React.ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [showChatInput, setShowChatInput] = useState(false);
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectConfig | null>(null);
|
||||
|
||||
// Auto collapse when messages appear
|
||||
useEffect(() => {
|
||||
if (hasMessages) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [hasMessages]);
|
||||
|
||||
const handleAddProject = () => {
|
||||
const modal = new AddProjectModal(app, async (project: ProjectConfig) => {
|
||||
onProjectAdded(project);
|
||||
});
|
||||
modal.open();
|
||||
};
|
||||
|
||||
const handleEditProject = (originP: ProjectConfig) => {
|
||||
const modal = new AddProjectModal(
|
||||
app,
|
||||
async (updatedProject: ProjectConfig) => {
|
||||
onEditProject(originP, updatedProject);
|
||||
if (selectedProject && selectedProject.name === originP.name) {
|
||||
setSelectedProject(updatedProject);
|
||||
}
|
||||
},
|
||||
originP
|
||||
);
|
||||
modal.open();
|
||||
};
|
||||
|
||||
const enableOrDisableProject = (enable: boolean, project?: ProjectConfig) => {
|
||||
if (!enable) {
|
||||
setSelectedProject(null);
|
||||
setShowChatInput(false);
|
||||
setIsOpen(true);
|
||||
showChatUI(false);
|
||||
setCurrentProject(null);
|
||||
return;
|
||||
} else {
|
||||
if (!project) {
|
||||
logError("Must be exist one project.");
|
||||
return;
|
||||
}
|
||||
setSelectedProject(project);
|
||||
setShowChatInput(true);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadContext = (p: ProjectConfig) => {
|
||||
setSelectedProject(p);
|
||||
setShowChatInput(true);
|
||||
setIsOpen(false);
|
||||
showChatUI(true);
|
||||
setCurrentProject(p);
|
||||
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="flex flex-col">
|
||||
{showChatInput && selectedProject ? (
|
||||
<div className="flex justify-between items-center px-2 py-3">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="font-semibold text-normal">Projects</span>
|
||||
<Select
|
||||
value={selectedProject.name}
|
||||
onValueChange={(value) => {
|
||||
const project = projects.find((p) => p.name === value);
|
||||
if (project) {
|
||||
handleLoadContext(project);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="size-4 text-accent/70" />
|
||||
<span>{selectedProject.name}</span>
|
||||
</div>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.name} value={project.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="size-4" />
|
||||
<span>{project.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="fit"
|
||||
variant="ghost2"
|
||||
onClick={() => handleEditProject(selectedProject)}
|
||||
className="hover:bg-accent/50 hover:text-on-accent"
|
||||
>
|
||||
<Edit2 className="size-4 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Edit Current Project</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
enableOrDisableProject(false);
|
||||
}}
|
||||
aria-label="Close Current Project"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Close Current Project</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className="transition-all duration-200 ease-in-out"
|
||||
>
|
||||
<div className="flex justify-between items-center px-4 py-3">
|
||||
<div className="flex gap-2 items-center flex-1">
|
||||
<span className="font-semibold text-normal">Projects</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="size-4 text-muted" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="w-64">
|
||||
Manage your projects with different contexts and configurations.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="fit" onClick={handleAddProject}>
|
||||
Create
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Add New Project</TooltipContent>
|
||||
</Tooltip>
|
||||
{projects.length > 0 && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost2" size="icon">
|
||||
{isOpen ? (
|
||||
<ChevronUp className="size-5" />
|
||||
) : (
|
||||
<ChevronDown className="size-5" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => onClose()}
|
||||
aria-label="close project mode"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Close Project Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{projects.length === 0 && (
|
||||
<div className="px-4 py-2 text-xs text-muted bg-secondary/30">
|
||||
No projects available
|
||||
</div>
|
||||
)}
|
||||
<CollapsibleContent className="transition-all duration-200 ease-in-out">
|
||||
<div className="relative bg-secondary/30">
|
||||
<div className="px-4 pt-3 pb-6 max-h-[calc(3*5.7rem)] overflow-y-auto">
|
||||
<div className="flex flex-col gap-2 @2xl:grid @2xl:grid-cols-2 @4xl:grid-cols-3">
|
||||
{projects.map((project) => (
|
||||
<ProjectItem
|
||||
key={project.name}
|
||||
project={project}
|
||||
loadContext={handleLoadContext}
|
||||
onEdit={handleEditProject}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{projects.length > 0 && (
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-6"
|
||||
style={{
|
||||
background: "linear-gradient(transparent, var(--background-primary) 75%)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showChatInput && (
|
||||
<div className="flex flex-col gap-4 items-center justify-center p-8 text-muted bg-secondary/30">
|
||||
<div className="max-w-[600px] space-y-4">
|
||||
<p className="text-base text-center">
|
||||
Create and explore personalized AI assistants with custom instructions, knowledge
|
||||
bases, and skill sets for each project.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="size-4" />
|
||||
<span>Double-click a project card to start chatting</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="size-4" />
|
||||
<span>
|
||||
The more you use a project, the deeper the AI's understanding becomes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ProjectList.displayName = "ProjectList";
|
||||
|
|
@ -65,6 +65,7 @@ const PROMPT_KEYS: Record<ChainType, Array<keyof typeof SUGGESTED_PROMPTS>> = {
|
|||
[ChainType.LLM_CHAIN]: ["activeNote", "quoteNote", "fun"],
|
||||
[ChainType.VAULT_QA_CHAIN]: ["qaVault", "qaVault", "quoteNote"],
|
||||
[ChainType.COPILOT_PLUS_CHAIN]: ["copilotPlus", "copilotPlus", "copilotPlus"],
|
||||
[ChainType.PROJECT_CHAIN]: ["copilotPlus", "copilotPlus", "copilotPlus"],
|
||||
};
|
||||
|
||||
function getRandomPrompt(chainType: ChainType = ChainType.LLM_CHAIN) {
|
||||
|
|
|
|||
393
src/components/modals/AddProjectModal.tsx
Normal file
393
src/components/modals/AddProjectModal.tsx
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { App, Modal, Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { err2String, checkModelApiKey, randomUUID } from "@/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FormField } from "@/components/ui/form-field";
|
||||
import { ProjectConfig } from "@/aiParams";
|
||||
import { useSettingsValue, getModelKeyFromModel } from "@/settings/model";
|
||||
import { getModelDisplayWithIcons } from "@/components/ui/model-display";
|
||||
import { ObsidianNativeSelect } from "@/components/ui/obsidian-native-select";
|
||||
import { SettingSlider } from "@/components/ui/setting-slider";
|
||||
import { PatternMatchingModal } from "@/components/modals/PatternMatchingModal";
|
||||
|
||||
interface AddProjectModalContentProps {
|
||||
initialProject?: ProjectConfig;
|
||||
onSave: (project: ProjectConfig) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProjectModalContentProps) {
|
||||
const settings = useSettingsValue();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [touched, setTouched] = useState({
|
||||
name: false,
|
||||
systemPrompt: false,
|
||||
projectModelKey: false,
|
||||
inclusions: false,
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState<Partial<ProjectConfig>>(
|
||||
initialProject || {
|
||||
id: randomUUID(),
|
||||
name: "",
|
||||
description: "",
|
||||
systemPrompt: "",
|
||||
projectModelKey: "",
|
||||
modelConfigs: {
|
||||
temperature: 1.0,
|
||||
maxTokens: 1000,
|
||||
},
|
||||
contextSource: {
|
||||
inclusions: "",
|
||||
exclusions: "",
|
||||
webUrls: "",
|
||||
youtubeUrls: "",
|
||||
},
|
||||
created: Date.now(),
|
||||
UsageTimestamps: Date.now(),
|
||||
}
|
||||
);
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
formData.name &&
|
||||
formData.systemPrompt &&
|
||||
formData.projectModelKey &&
|
||||
formData.contextSource?.inclusions?.trim()
|
||||
);
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | string[] | Record<string, any>
|
||||
) => {
|
||||
setFormData((prev) => {
|
||||
// Handle text input
|
||||
if (typeof value === "string") {
|
||||
value = value.trim();
|
||||
}
|
||||
// Handle string arrays
|
||||
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
||||
value = value.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
if (field.includes(".")) {
|
||||
const [parent, child] = field.split(".");
|
||||
const parentKey = parent as keyof typeof prev;
|
||||
const parentValue = prev[parentKey];
|
||||
|
||||
if (typeof parentValue === "object" && parentValue !== null) {
|
||||
return {
|
||||
...prev,
|
||||
[parent]: {
|
||||
...parentValue,
|
||||
[child]: value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[field]: value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const requiredFields = ["name", "systemPrompt", "projectModelKey"];
|
||||
const missingFields = requiredFields.filter((field) => !formData[field as keyof ProjectConfig]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
setTouched((prev) => ({
|
||||
...prev,
|
||||
...Object.fromEntries(missingFields.map((field) => [field, true])),
|
||||
}));
|
||||
new Notice("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onSave(formData as ProjectConfig);
|
||||
} catch (e) {
|
||||
new Notice(err2String(e));
|
||||
setTouched((prev) => ({
|
||||
...prev,
|
||||
name: true,
|
||||
}));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<div className="text-xl font-bold text-normal mb-2">Add New Project</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
label="Project Name"
|
||||
required
|
||||
error={touched.name && !formData.name}
|
||||
errorMessage="Project name is required"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, name: true }))}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Description"
|
||||
description="Briefly describe the purpose and goals of the project"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Prompt"
|
||||
required
|
||||
error={touched.systemPrompt && !formData.systemPrompt}
|
||||
errorMessage="System prompt is required"
|
||||
>
|
||||
<Textarea
|
||||
value={formData.systemPrompt}
|
||||
onChange={(e) => handleInputChange("systemPrompt", e.target.value)}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, systemPrompt: true }))}
|
||||
className="min-h-[8rem]"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Default Model"
|
||||
required
|
||||
error={touched.projectModelKey && !formData.projectModelKey}
|
||||
errorMessage="Default model is required"
|
||||
>
|
||||
<ObsidianNativeSelect
|
||||
value={formData.projectModelKey}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const selectedModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === value
|
||||
);
|
||||
if (!selectedModel) return;
|
||||
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(selectedModel, settings);
|
||||
if (!hasApiKey && errorNotice) {
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
handleInputChange("projectModelKey", value);
|
||||
}}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, projectModelKey: true }))}
|
||||
placeholder="Select a model"
|
||||
options={settings.activeModels
|
||||
.filter((m) => m.enabled)
|
||||
.map((model) => ({
|
||||
label: getModelDisplayWithIcons(model),
|
||||
value: getModelKeyFromModel(model),
|
||||
}))}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-base font-medium">Model Configuration</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<FormField label="Temperature">
|
||||
<SettingSlider
|
||||
value={formData.modelConfigs?.temperature ?? 1}
|
||||
onChange={(value) => handleInputChange("modelConfigs.temperature", value)}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.01}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Token Limit">
|
||||
<SettingSlider
|
||||
value={formData.modelConfigs?.maxTokens ?? 1000}
|
||||
onChange={(value) => handleInputChange("modelConfigs.maxTokens", value)}
|
||||
min={1}
|
||||
max={16000}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-base font-medium">Context Sources</div>
|
||||
<FormField
|
||||
label="Inclusions"
|
||||
required
|
||||
error={touched.inclusions && !formData.contextSource?.inclusions?.trim()}
|
||||
errorMessage="At least one inclusion pattern is required"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 text-xs text-muted">
|
||||
{formData.contextSource?.inclusions?.trim()
|
||||
? "Patterns configured"
|
||||
: "No patterns configured"}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
new PatternMatchingModal(
|
||||
app,
|
||||
(value: string) => {
|
||||
handleInputChange("contextSource.inclusions", value);
|
||||
setTouched((prev) => ({ ...prev, inclusions: true }));
|
||||
},
|
||||
formData.contextSource?.inclusions || "",
|
||||
"Manage Inclusions"
|
||||
).open()
|
||||
}
|
||||
>
|
||||
Manage Patterns
|
||||
</Button>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Exclusions"
|
||||
description="Exclude specific files or patterns from the included folders above"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 text-xs text-muted">
|
||||
{formData.contextSource?.exclusions?.trim()
|
||||
? "Patterns configured"
|
||||
: "No patterns configured"}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
new PatternMatchingModal(
|
||||
app,
|
||||
(value: string) => {
|
||||
handleInputChange("contextSource.exclusions", value);
|
||||
},
|
||||
formData.contextSource?.exclusions || "",
|
||||
"Manage Exclusions"
|
||||
).open()
|
||||
}
|
||||
>
|
||||
Manage Patterns
|
||||
</Button>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Web URLs">
|
||||
<Textarea
|
||||
value={formData.contextSource?.webUrls}
|
||||
onChange={(e) => {
|
||||
const urls = e.target.value
|
||||
.split("\n")
|
||||
.map((url) => url.trim())
|
||||
.filter((url) => {
|
||||
if (!url) return false;
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
handleInputChange("contextSource.webUrls", urls);
|
||||
}}
|
||||
placeholder="Enter web URLs, one per line"
|
||||
className="min-h-[80px] w-full"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="YouTube URLs">
|
||||
<Textarea
|
||||
value={formData.contextSource?.youtubeUrls}
|
||||
onChange={(e) => {
|
||||
const urls = e.target.value
|
||||
.split("\n")
|
||||
.map((url) => url.trim())
|
||||
.filter((url) => {
|
||||
if (!url) return false;
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return (
|
||||
urlObj.hostname.includes("youtube.com") ||
|
||||
urlObj.hostname.includes("youtu.be")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
handleInputChange("contextSource.youtubeUrls", urls);
|
||||
}}
|
||||
placeholder="Enter YouTube URLs, one per line"
|
||||
className="min-h-[80px] w-full"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 mt-4">
|
||||
<Button variant="ghost" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSubmitting || !isFormValid()}>
|
||||
{isSubmitting ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class AddProjectModal extends Modal {
|
||||
private root: Root;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private onSave: (project: ProjectConfig) => Promise<void>,
|
||||
private initialProject?: ProjectConfig
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.root = createRoot(contentEl);
|
||||
|
||||
const handleSave = async (project: ProjectConfig) => {
|
||||
await this.onSave(project);
|
||||
this.close();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
this.close();
|
||||
};
|
||||
|
||||
this.root.render(
|
||||
<AddProjectModalContent
|
||||
initialProject={this.initialProject}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { useModelKey } from "@/aiParams";
|
||||
import { processCommandPrompt } from "@/commands/inlineEditCommandUtils";
|
||||
import { getModelDisplayText } from "@/components/ui/model-display";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import { InlineEditCommandSettings, useSettingsValue } from "@/settings/model";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { findCustomModel, insertIntoEditor } from "@/utils";
|
||||
|
|
@ -9,6 +8,7 @@ import { Bot, Copy, PenLine } from "lucide-react";
|
|||
import { App, Modal, Notice } from "obsidian";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
interface InlineEditModalContentProps {
|
||||
originalText: string;
|
||||
|
|
@ -44,7 +44,9 @@ function InlineEditModalContent({
|
|||
async function stream() {
|
||||
const prompt = processCommandPrompt(command.prompt, originalText);
|
||||
let fullAIResponse = "";
|
||||
const chatModel = await ChatModelManager.getInstance().createModelInstance(selectedModel);
|
||||
const chatModel = await ProjectManager.instance
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.createModelInstance(selectedModel);
|
||||
const chatStream = await chatModel.stream(prompt);
|
||||
for await (const chunk of chatStream) {
|
||||
if (abortController?.signal.aborted) break;
|
||||
|
|
|
|||
59
src/components/ui/obsidian-native-select.tsx
Normal file
59
src/components/ui/obsidian-native-select.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ObsidianNativeSelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export function ObsidianNativeSelect({
|
||||
options,
|
||||
placeholder,
|
||||
containerClassName,
|
||||
className,
|
||||
...props
|
||||
}: ObsidianNativeSelectProps) {
|
||||
return (
|
||||
<div className={cn("relative w-full group", containerClassName)}>
|
||||
<select
|
||||
className={cn(
|
||||
"w-full appearance-none",
|
||||
"flex h-9 rounded-md border border-solid border-border bg-dropdown px-3 py-1 pr-8",
|
||||
"text-sm !shadow transition-colors",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"hover:bg-interactive-hover hover:text-normal",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2",
|
||||
"transition-colors group-hover:[&>svg]:text-normal",
|
||||
props.disabled && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"!shadow-none",
|
||||
"data-[state=active]:!bg-interactive-accent data-[state=active]:!text-on-accent data-[state=active]:!shadow-md",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-ring transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
|
@ -496,6 +496,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
|
|||
promptUsageTimestamps: {},
|
||||
defaultConversationNoteName: "{$topic}@{$date}_{$time}",
|
||||
inlineEditCommands: DEFAULT_INLINE_EDIT_COMMANDS,
|
||||
projectList: [],
|
||||
};
|
||||
|
||||
export const EVENT_NAMES = {
|
||||
|
|
|
|||
18
src/main.ts
18
src/main.ts
|
|
@ -1,5 +1,5 @@
|
|||
import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
||||
import ChainManager from "@/LLMProviders/chainManager";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { parseChatContent, updateChatMemory } from "@/chatUtils";
|
||||
import { registerCommands } from "@/commands";
|
||||
|
|
@ -32,12 +32,13 @@ import {
|
|||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import { IntentAnalyzer } from "./LLMProviders/intentAnalyzer";
|
||||
import { logInfo } from "@/logger";
|
||||
|
||||
export default class CopilotPlugin extends Plugin {
|
||||
// A chat history that stores the messages sent and received
|
||||
// Only reset when the user explicitly clicks "New Chat"
|
||||
sharedState: SharedState;
|
||||
chainManager: ChainManager;
|
||||
projectManager: ProjectManager;
|
||||
brevilabsClient: BrevilabsClient;
|
||||
userMessageHistory: string[] = [];
|
||||
vectorStoreManager: VectorStoreManager;
|
||||
|
|
@ -55,8 +56,9 @@ export default class CopilotPlugin extends Plugin {
|
|||
registerCommands(this, prev, next);
|
||||
});
|
||||
this.addSettingTab(new CopilotSettingTab(this.app, this));
|
||||
// Always have one instance of sharedState and chainManager in the plugin
|
||||
this.sharedState = new SharedState();
|
||||
|
||||
// Always have one instance of sharedState in the plugin
|
||||
this.sharedState = new SharedState(this);
|
||||
|
||||
this.vectorStoreManager = VectorStoreManager.getInstance();
|
||||
|
||||
|
|
@ -65,7 +67,8 @@ export default class CopilotPlugin extends Plugin {
|
|||
this.brevilabsClient.setPluginVersion(this.manifest.version);
|
||||
checkIsPlusUser();
|
||||
|
||||
this.chainManager = new ChainManager(this.app, this.vectorStoreManager);
|
||||
// Initialize ProjectManager
|
||||
this.projectManager = ProjectManager.getInstance(this.app, this.vectorStoreManager, this);
|
||||
|
||||
// Initialize FileParserManager early with other core services
|
||||
this.fileParserManager = new FileParserManager(this.brevilabsClient);
|
||||
|
|
@ -115,9 +118,10 @@ export default class CopilotPlugin extends Plugin {
|
|||
if (this.vectorStoreManager) {
|
||||
this.vectorStoreManager.onunload();
|
||||
}
|
||||
|
||||
this.settingsUnsubscriber?.();
|
||||
|
||||
console.log("Copilot plugin unloaded");
|
||||
logInfo("Copilot plugin unloaded");
|
||||
}
|
||||
|
||||
updateUserMessageHistory(newMessage: string) {
|
||||
|
|
@ -300,7 +304,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
messages.forEach((message) => this.sharedState.addMessage(message));
|
||||
|
||||
// Update the chain's memory with the loaded messages
|
||||
await updateChatMemory(messages, this.chainManager.memoryManager);
|
||||
await updateChatMemory(messages, this.projectManager.getCurrentChainManager().memoryManager);
|
||||
|
||||
// Check if the Copilot view is already active
|
||||
const existingView = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { logInfo } from "@/logger";
|
||||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
|
|
@ -13,6 +12,7 @@ import { BaseRetriever } from "@langchain/core/retrievers";
|
|||
import { search } from "@orama/orama";
|
||||
import { TFile } from "obsidian";
|
||||
import { DBOperations } from "./dbOperations";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
export class HybridRetriever extends BaseRetriever {
|
||||
public lc_namespace = ["hybrid_retriever"];
|
||||
|
|
@ -130,8 +130,9 @@ export class HybridRetriever extends BaseRetriever {
|
|||
private async rewriteQuery(query: string): Promise<string> {
|
||||
try {
|
||||
const promptResult = await this.queryRewritePrompt.format({ question: query });
|
||||
const chatModel = ChatModelManager.getInstance()
|
||||
.getChatModel()
|
||||
const chatModel = ProjectManager.instance
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.getChatModel()
|
||||
.bind({ temperature: 0 } as BaseChatModelCallOptions);
|
||||
const rewrittenQueryObject = await chatModel.invoke(promptResult);
|
||||
|
||||
|
|
|
|||
|
|
@ -94,18 +94,24 @@ function getInclusionPatterns(): string[] {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the inclusion and exclusion patterns from the settings.
|
||||
* Get the inclusion and exclusion patterns from the settings or provided values.
|
||||
* @param options - Optional parameters for inclusions and exclusions.
|
||||
* @returns An object containing the inclusions and exclusions patterns strings.
|
||||
*/
|
||||
export function getMatchingPatterns(): {
|
||||
export function getMatchingPatterns(options?: { inclusions?: string; exclusions?: string }): {
|
||||
inclusions: PatternCategory | null;
|
||||
exclusions: PatternCategory | null;
|
||||
} {
|
||||
const inclusions = getInclusionPatterns();
|
||||
const exclusions = getExclusionPatterns();
|
||||
const inclusionPatterns = options?.inclusions
|
||||
? getDecodedPatterns(options.inclusions)
|
||||
: getInclusionPatterns();
|
||||
const exclusionPatterns = options?.exclusions
|
||||
? getDecodedPatterns(options.exclusions)
|
||||
: getExclusionPatterns();
|
||||
|
||||
return {
|
||||
inclusions: inclusions.length > 0 ? categorizePatterns(inclusions) : null,
|
||||
exclusions: exclusions.length > 0 ? categorizePatterns(exclusions) : null,
|
||||
inclusions: inclusionPatterns.length > 0 ? categorizePatterns(inclusionPatterns) : null,
|
||||
exclusions: exclusionPatterns.length > 0 ? categorizePatterns(exclusionPatterns) : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CustomModel } from "@/aiParams";
|
||||
import { CustomModel, ProjectConfig } from "@/aiParams";
|
||||
import { atom, createStore, useAtomValue } from "jotai";
|
||||
|
||||
import { type ChainType } from "@/chainFactory";
|
||||
|
|
@ -89,6 +89,7 @@ export interface CopilotSettings {
|
|||
// undefined means never checked
|
||||
isPlusUser: boolean | undefined;
|
||||
inlineEditCommands: InlineEditCommandSettings[] | undefined;
|
||||
projectList: Array<ProjectConfig>;
|
||||
}
|
||||
|
||||
export const settingsStore = createStore();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -15,6 +14,7 @@ import { err2String, getNeedSetKeyProvider, getProviderInfo, getProviderLabel }
|
|||
import { Loader2 } from "lucide-react";
|
||||
import { Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
interface ApiKeyDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -96,7 +96,7 @@ const ApiKeyDialog: React.FC<ApiKeyDialogProps> = ({
|
|||
apiKey,
|
||||
enabled: true,
|
||||
};
|
||||
await ChatModelManager.getInstance().ping(customModel);
|
||||
await ProjectManager.instance.getCurrentChainManager().chatModelManager.ping(customModel);
|
||||
|
||||
new Notice("API key verified successfully!");
|
||||
setUnverifiedKeys((prev) => {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const ChainType2Label: Record<ChainType, string> = {
|
|||
[ChainType.LLM_CHAIN]: "Chat",
|
||||
[ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)",
|
||||
[ChainType.COPILOT_PLUS_CHAIN]: "Copilot Plus (beta)",
|
||||
[ChainType.PROJECT_CHAIN]: "Project",
|
||||
};
|
||||
|
||||
export const BasicSettings: React.FC = () => {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import React, { useState } from "react";
|
|||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { setSettings, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { ModelAddDialog } from "@/settings/v2/components/ModelAddDialog";
|
||||
import { ModelTable } from "@/settings/v2/components/ModelTable";
|
||||
import { ModelEditDialog } from "@/settings/v2/components/ModelEditDialog";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
export const ModelSettings: React.FC = () => {
|
||||
const settings = useSettingsValue();
|
||||
|
|
@ -94,7 +94,9 @@ export const ModelSettings: React.FC = () => {
|
|||
const updatedModels = [...settings.activeModels, model];
|
||||
updateSetting("activeModels", updatedModels);
|
||||
}}
|
||||
ping={(model) => ChatModelManager.getInstance().ping(model)}
|
||||
ping={(model) =>
|
||||
ProjectManager.instance.getCurrentChainManager().chatModelManager.ping(model)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FormattedDateTime } from "./utils";
|
||||
import { TFile } from "obsidian";
|
||||
import CopilotPlugin from "@/main";
|
||||
|
||||
export interface ChatMessage {
|
||||
message: string;
|
||||
|
|
@ -18,10 +19,14 @@ export interface ChatMessage {
|
|||
}
|
||||
|
||||
class SharedState {
|
||||
constructor(private plugin: CopilotPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
chatHistory: ChatMessage[] = [];
|
||||
|
||||
addMessage(message: ChatMessage): void {
|
||||
this.chatHistory.push(message);
|
||||
this.plugin.projectManager.getCurrentChainManager().addChatMessage(message);
|
||||
}
|
||||
|
||||
getMessages(): ChatMessage[] {
|
||||
|
|
@ -30,6 +35,11 @@ class SharedState {
|
|||
|
||||
clearChatHistory(): void {
|
||||
this.chatHistory = [];
|
||||
this.plugin.projectManager.getCurrentChainManager().clearHistory();
|
||||
}
|
||||
|
||||
replaceMessages(messages: ChatMessage[]): void {
|
||||
this.chatHistory = [...messages];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,8 +58,8 @@ export function useSharedState(
|
|||
// When there are no dependencies, the effect will only run once,
|
||||
// *right after the initial render* (similar to componentDidMount in class components).
|
||||
useEffect(() => {
|
||||
setChatHistory(sharedState.getMessages());
|
||||
}, [sharedState]);
|
||||
setChatHistory([...sharedState.getMessages()]);
|
||||
}, [sharedState, sharedState.chatHistory]);
|
||||
|
||||
const addMessage = useCallback(
|
||||
(message: ChatMessage) => {
|
||||
|
|
|
|||
|
|
@ -90,16 +90,6 @@ If your plugin does not need CSS, delete this file.
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-context-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -925,3 +925,7 @@ export function checkModelApiKey(
|
|||
export function removeThinkTags(text: string): string {
|
||||
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
||||
}
|
||||
|
||||
export function randomUUID() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue