add mode custom

This commit is contained in:
duanfuxiang 2025-04-28 23:03:53 +08:00
parent 497a9739d7
commit f282c9f667
7 changed files with 103 additions and 23 deletions

View File

@ -30,6 +30,7 @@ import {
} from '../../core/llm/exception'
import { regexSearchFiles } from '../../core/ripgrep'
import { useChatHistory } from '../../hooks/use-chat-history'
import { useCustomModes } from '../../hooks/use-custom-mode'
import { ApplyStatus, ToolArgs } from '../../types/apply'
import { ChatMessage, ChatUserMessage } from '../../types/chat'
import {
@ -101,6 +102,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
const { settings, setSettings } = useSettings()
const { getRAGEngine } = useRAG()
const diffStrategy = useDiffStrategy()
const { customModeList, customModePrompts } = useCustomModes()
const {
createOrUpdateConversation,
@ -112,8 +114,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
const { streamResponse, chatModel } = useLLM()
const promptGenerator = useMemo(() => {
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy)
}, [getRAGEngine, app, settings, diffStrategy])
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList)
}, [getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList])
const [inputMessage, setInputMessage] = useState<ChatUserMessage>(() => {
const newMessage = getNewInputMessage(app, settings.defaultMention)

View File

@ -1,10 +1,14 @@
import { Plus, Undo2, Settings, Circle, Trash2 } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { ChevronDown, ChevronRight, Plus, Trash2, Undo2 } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { useApp } from '../../contexts/AppContext';
import { CustomMode, GroupEntry, ToolGroup } from '../../database/json/custom-mode/types';
import { useCustomModes } from '../../hooks/use-custom-mode';
import { CustomMode, ToolGroup, toolGroups, GroupEntry } from '../../database/json/custom-mode/types';
import { modes as buildinModes } from '../../utils/modes';
import { openOrCreateMarkdownFile } from '../../utils/obsidian';
const CustomModeView = () => {
const app = useApp()
const {
createCustomMode,
deleteCustomMode,
@ -15,7 +19,7 @@ const CustomModeView = () => {
// 当前选择的模式
const [selectedMode, setSelectedMode] = useState<string>('ask')
const [isBuiltinMode, setIsBuiltinMode] = useState<boolean>(true)
const [isAdvancedCollapsed, setIsAdvancedCollapsed] = useState(true);
const isNewMode = React.useMemo(() => selectedMode === "add_new_mode", [selectedMode])
@ -46,7 +50,6 @@ const CustomModeView = () => {
// 自定义指令
const [customInstructions, setCustomInstructions] = useState<string>('')
// 当模式变更时更新表单数据
useEffect(() => {
// new mode
@ -63,7 +66,7 @@ const CustomModeView = () => {
const builtinMode = buildinModes.find(m => m.slug === selectedMode);
if (builtinMode) {
setIsBuiltinMode(true);
setModeName(builtinMode.name);
setModeName(builtinMode.slug);
setRoleDefinition(builtinMode.roleDefinition);
setCustomInstructions(builtinMode.customInstructions || '');
setSelectedTools(builtinMode.groups as GroupEntry[]);
@ -219,9 +222,11 @@ const CustomModeView = () => {
<div className="infio-custom-modes-section">
<div className="infio-section-header">
<h3></h3>
<button className="infio-section-btn">
<Undo2 size={16} />
</button>
{isBuiltinMode && (
<button className="infio-section-btn">
<Undo2 size={16} />
</button>
)}
</div>
<p className="infio-section-subtitle"></p>
<textarea
@ -274,7 +279,7 @@ const CustomModeView = () => {
checked={selectedTools.includes('research')}
onChange={() => handleToolChange('research')}
/>
</label>
</div>
</div>
@ -298,10 +303,29 @@ const CustomModeView = () => {
placeholder="输入模式自定义指令..."
/>
<p className="infio-section-footer">
<a href="#" className="infio-link">_infio_prompts/code-rules/</a>
<a href="#" className="infio-link" onClick={() => openOrCreateMarkdownFile(app, `_infio_prompts/${modeName}/rules.md`, 0)}>_infio_prompts/{modeName}/rules</a>
</p>
</div>
{/* 高级, 覆盖系统提示词 */}
<div className="infio-custom-modes-section">
<div
className="infio-section-header infio-section-header-collapsible"
onClick={() => setIsAdvancedCollapsed(!isAdvancedCollapsed)}
>
<div className="infio-section-header-title-container">
{isAdvancedCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
<h6></h6>
</div>
</div>
{!isAdvancedCollapsed && (
<p className="infio-section-subtitle">
<a href="#" className="infio-link" onClick={() => openOrCreateMarkdownFile(app, `_infio_prompts/${modeName}/system-prompt.md`, 0)}>_infio_prompts/{modeName}/system-prompt</a>
使,
</p>
)}
</div>
<div className="infio-custom-modes-actions">
<button className="infio-preview-btn">
@ -546,6 +570,17 @@ const CustomModeView = () => {
justify-content: center;
width: fit-content;
}
.infio-section-header-collapsible {
cursor: pointer;
user-select: none;
}
.infio-section-header-title-container {
display: flex;
align-items: center;
gap: 4px;
}
`}
</style>
</div>

View File

@ -1,8 +1,9 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { useEffect, useState } from 'react'
import React, { useEffect, useState, useMemo } from 'react'
import { useSettings } from '../../../contexts/SettingsContext'
import { useCustomModes } from '../../../hooks/use-custom-mode'
import { modes } from '../../../utils/modes'
export function ModeSelect() {
@ -10,11 +11,14 @@ export function ModeSelect() {
const [isOpen, setIsOpen] = useState(false)
const [mode, setMode] = useState(settings.mode)
const { customModeList } = useCustomModes()
const allModes = useMemo(() => [...modes, ...customModeList], [customModeList])
useEffect(() => {
setMode(settings.mode)
}, [settings.mode])
return (
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu.Trigger className="infio-chat-input-model-select">
@ -22,7 +26,7 @@ export function ModeSelect() {
{isOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</div>
<div className="infio-chat-input-model-select__model-name">
{modes.find((m) => m.slug === mode)?.name}
{allModes.find((m) => m.slug === mode)?.name}
</div>
</DropdownMenu.Trigger>
@ -30,7 +34,7 @@ export function ModeSelect() {
<DropdownMenu.Content
className="infio-popover">
<ul>
{modes.map((mode) => (
{allModes.map((mode) => (
<DropdownMenu.Item
key={mode.slug}
onSelect={() => {

View File

@ -152,10 +152,10 @@ ${await addCustomInstructions(this.app, promptComponent?.customInstructions || m
filesSearchMethod: string = 'regex',
preferredLanguage?: string,
diffStrategy?: DiffStrategy,
mcpHub?: McpHub,
browserViewportSize?: string,
customModePrompts?: CustomModePrompts,
customModes?: ModeConfig[],
mcpHub?: McpHub,
browserViewportSize?: string,
globalCustomInstructions?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,

View File

@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useApp } from '../contexts/AppContext'
import { CustomModeManager } from '../database/json/custom-mode/CustomModeManager'
import { CustomMode, GroupEntry } from '../database/json/custom-mode/types'
import { CustomModePrompts } from '../utils/modes'
type UseCustomModes = {
createCustomMode: (
@ -22,6 +22,7 @@ type UseCustomModes = {
) => Promise<void>
FindCustomModeByName: (name: string) => Promise<CustomMode | undefined>
customModeList: CustomMode[]
customModePrompts: CustomModePrompts
}
export function useCustomModes(): UseCustomModes {
@ -37,6 +38,16 @@ export function useCustomModes(): UseCustomModes {
})
}, [customModeManager])
const customModePrompts = useMemo(() => {
return customModeList.reduce((acc, customMode) => {
acc[customMode.slug] = {
roleDefinition: customMode.roleDefinition,
customInstructions: customMode.customInstructions,
}
return acc
}, {} as CustomModePrompts)
}, [customModeList])
useEffect(() => {
void fetchCustomModeList()
}, [fetchCustomModeList])
@ -91,5 +102,6 @@ export function useCustomModes(): UseCustomModes {
updateCustomMode,
FindCustomModeByName,
customModeList,
customModePrompts,
}
}

View File

@ -1,5 +1,7 @@
import { App, Editor, MarkdownView, TFile, TFolder, Vault, WorkspaceLeaf } from 'obsidian'
import * as path from 'path'
import { MentionableBlockData } from '../types/mentionable'
export async function readTFileContent(
@ -61,7 +63,7 @@ export function getOpenFiles(app: App): TFile[] {
const leaves = app.workspace.getLeavesOfType('markdown')
return leaves
.filter((v): v is WorkspaceLeaf & { view: MarkdownView & { file: TFile } } =>
.filter((v): v is WorkspaceLeaf & { view: MarkdownView & { file: TFile } } =>
v.view instanceof MarkdownView && !!v.view.file
)
.map((v) => v.view.file)
@ -125,3 +127,20 @@ export function openMarkdownFile(
})
}
}
export async function openOrCreateMarkdownFile(
app: App,
filePath: string,
startLine?: number,
) {
const file_exists = await app.vault.adapter.exists(filePath)
if (!file_exists) {
const dir = path.dirname(filePath)
const dir_exists = await app.vault.adapter.exists(dir)
if (!dir_exists) {
await app.vault.adapter.mkdir(dir)
}
await app.vault.adapter.write(filePath, '')
}
openMarkdownFile(app, filePath, startLine)
}

View File

@ -17,7 +17,7 @@ import {
MentionableVault
} from '../types/mentionable'
import { InfioSettings } from '../types/settings'
import { Mode, getFullModeDetails } from "../utils/modes"
import { CustomModePrompts, Mode, ModeConfig, getFullModeDetails } from "../utils/modes"
import {
readTFileContent
@ -116,6 +116,8 @@ export class PromptGenerator {
private settings: InfioSettings
private diffStrategy: DiffStrategy
private systemPrompt: SystemPrompt
private customModePrompts: CustomModePrompts | null = null
private customModeList: ModeConfig[] | null = null
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
role: 'assistant',
content: '',
@ -126,12 +128,16 @@ export class PromptGenerator {
app: App,
settings: InfioSettings,
diffStrategy?: DiffStrategy,
customModePrompts?: CustomModePrompts,
customModeList?: ModeConfig[],
) {
this.getRagEngine = getRagEngine
this.app = app
this.settings = settings
this.diffStrategy = diffStrategy
this.systemPrompt = new SystemPrompt(this.app)
this.customModePrompts = customModePrompts ?? null
this.customModeList = customModeList ?? null
}
public async generateRequestMessages({
@ -473,7 +479,9 @@ export class PromptGenerator {
mode,
filesSearchMethod,
preferredLanguage,
this.diffStrategy
this.diffStrategy,
this.customModePrompts,
this.customModeList,
)
return {