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

View File

@ -1,10 +1,14 @@
import { Plus, Undo2, Settings, Circle, Trash2 } from 'lucide-react'; import { ChevronDown, ChevronRight, Plus, Trash2, Undo2 } from 'lucide-react';
import React, { useState, useEffect } from '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 { 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 { modes as buildinModes } from '../../utils/modes';
import { openOrCreateMarkdownFile } from '../../utils/obsidian';
const CustomModeView = () => { const CustomModeView = () => {
const app = useApp()
const { const {
createCustomMode, createCustomMode,
deleteCustomMode, deleteCustomMode,
@ -15,7 +19,7 @@ const CustomModeView = () => {
// 当前选择的模式 // 当前选择的模式
const [selectedMode, setSelectedMode] = useState<string>('ask') const [selectedMode, setSelectedMode] = useState<string>('ask')
const [isBuiltinMode, setIsBuiltinMode] = useState<boolean>(true) const [isBuiltinMode, setIsBuiltinMode] = useState<boolean>(true)
const [isAdvancedCollapsed, setIsAdvancedCollapsed] = useState(true);
const isNewMode = React.useMemo(() => selectedMode === "add_new_mode", [selectedMode]) const isNewMode = React.useMemo(() => selectedMode === "add_new_mode", [selectedMode])
@ -46,7 +50,6 @@ const CustomModeView = () => {
// 自定义指令 // 自定义指令
const [customInstructions, setCustomInstructions] = useState<string>('') const [customInstructions, setCustomInstructions] = useState<string>('')
// 当模式变更时更新表单数据 // 当模式变更时更新表单数据
useEffect(() => { useEffect(() => {
// new mode // new mode
@ -63,7 +66,7 @@ const CustomModeView = () => {
const builtinMode = buildinModes.find(m => m.slug === selectedMode); const builtinMode = buildinModes.find(m => m.slug === selectedMode);
if (builtinMode) { if (builtinMode) {
setIsBuiltinMode(true); setIsBuiltinMode(true);
setModeName(builtinMode.name); setModeName(builtinMode.slug);
setRoleDefinition(builtinMode.roleDefinition); setRoleDefinition(builtinMode.roleDefinition);
setCustomInstructions(builtinMode.customInstructions || ''); setCustomInstructions(builtinMode.customInstructions || '');
setSelectedTools(builtinMode.groups as GroupEntry[]); setSelectedTools(builtinMode.groups as GroupEntry[]);
@ -219,9 +222,11 @@ const CustomModeView = () => {
<div className="infio-custom-modes-section"> <div className="infio-custom-modes-section">
<div className="infio-section-header"> <div className="infio-section-header">
<h3></h3> <h3></h3>
<button className="infio-section-btn"> {isBuiltinMode && (
<Undo2 size={16} /> <button className="infio-section-btn">
</button> <Undo2 size={16} />
</button>
)}
</div> </div>
<p className="infio-section-subtitle"></p> <p className="infio-section-subtitle"></p>
<textarea <textarea
@ -274,7 +279,7 @@ const CustomModeView = () => {
checked={selectedTools.includes('research')} checked={selectedTools.includes('research')}
onChange={() => handleToolChange('research')} onChange={() => handleToolChange('research')}
/> />
</label> </label>
</div> </div>
</div> </div>
@ -298,10 +303,29 @@ const CustomModeView = () => {
placeholder="输入模式自定义指令..." placeholder="输入模式自定义指令..."
/> />
<p className="infio-section-footer"> <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> </p>
</div> </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"> <div className="infio-custom-modes-actions">
<button className="infio-preview-btn"> <button className="infio-preview-btn">
@ -546,6 +570,17 @@ const CustomModeView = () => {
justify-content: center; justify-content: center;
width: fit-content; 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> </style>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { App, Editor, MarkdownView, TFile, TFolder, Vault, WorkspaceLeaf } from 'obsidian' import { App, Editor, MarkdownView, TFile, TFolder, Vault, WorkspaceLeaf } from 'obsidian'
import * as path from 'path'
import { MentionableBlockData } from '../types/mentionable' import { MentionableBlockData } from '../types/mentionable'
export async function readTFileContent( export async function readTFileContent(
@ -61,7 +63,7 @@ export function getOpenFiles(app: App): TFile[] {
const leaves = app.workspace.getLeavesOfType('markdown') const leaves = app.workspace.getLeavesOfType('markdown')
return leaves 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 v.view instanceof MarkdownView && !!v.view.file
) )
.map((v) => 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 MentionableVault
} from '../types/mentionable' } from '../types/mentionable'
import { InfioSettings } from '../types/settings' import { InfioSettings } from '../types/settings'
import { Mode, getFullModeDetails } from "../utils/modes" import { CustomModePrompts, Mode, ModeConfig, getFullModeDetails } from "../utils/modes"
import { import {
readTFileContent readTFileContent
@ -116,6 +116,8 @@ export class PromptGenerator {
private settings: InfioSettings private settings: InfioSettings
private diffStrategy: DiffStrategy private diffStrategy: DiffStrategy
private systemPrompt: SystemPrompt private systemPrompt: SystemPrompt
private customModePrompts: CustomModePrompts | null = null
private customModeList: ModeConfig[] | null = null
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = { private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
role: 'assistant', role: 'assistant',
content: '', content: '',
@ -126,12 +128,16 @@ export class PromptGenerator {
app: App, app: App,
settings: InfioSettings, settings: InfioSettings,
diffStrategy?: DiffStrategy, diffStrategy?: DiffStrategy,
customModePrompts?: CustomModePrompts,
customModeList?: ModeConfig[],
) { ) {
this.getRagEngine = getRagEngine this.getRagEngine = getRagEngine
this.app = app this.app = app
this.settings = settings this.settings = settings
this.diffStrategy = diffStrategy this.diffStrategy = diffStrategy
this.systemPrompt = new SystemPrompt(this.app) this.systemPrompt = new SystemPrompt(this.app)
this.customModePrompts = customModePrompts ?? null
this.customModeList = customModeList ?? null
} }
public async generateRequestMessages({ public async generateRequestMessages({
@ -473,7 +479,9 @@ export class PromptGenerator {
mode, mode,
filesSearchMethod, filesSearchMethod,
preferredLanguage, preferredLanguage,
this.diffStrategy this.diffStrategy,
this.customModePrompts,
this.customModeList,
) )
return { return {