mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 16:31:56 +00:00
feat: update custom mode draft
This commit is contained in:
parent
5558c96aa1
commit
497a9739d7
@ -97,6 +97,7 @@
|
||||
"p-limit": "^6.1.0",
|
||||
"parse5": "^7.1.2",
|
||||
"path": "^0.12.7",
|
||||
"radix-ui": "^1.3.4",
|
||||
"react": "^18.3.1",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
1703
pnpm-lock.yaml
generated
1703
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { CircleStop, History, Plus, SquareSlash } from 'lucide-react'
|
||||
import { CircleStop, History, NotebookPen, Plus, SquareSlash } from 'lucide-react'
|
||||
import { App, Notice } from 'obsidian'
|
||||
import {
|
||||
forwardRef,
|
||||
@ -54,6 +54,7 @@ import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInp
|
||||
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatHistory } from './ChatHistoryView'
|
||||
import CommandsView from './CommandsView'
|
||||
import CustomModeView from './CustomModeView'
|
||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||
import ReactMarkdown from './ReactMarkdown'
|
||||
@ -161,7 +162,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands'>('chat')
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode'>('custom-mode')
|
||||
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
@ -935,6 +936,19 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<SquareSlash size={18} color={tab === 'commands' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// switch between chat and prompts
|
||||
if (tab === 'custom-mode') {
|
||||
setTab('chat')
|
||||
} else {
|
||||
setTab('custom-mode')
|
||||
}
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<NotebookPen size={18} color={tab === 'custom-mode' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* main view */}
|
||||
@ -1047,12 +1061,16 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
addedBlockKey={addedBlockKey}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
) : tab === 'commands' ? (
|
||||
<div className="infio-chat-commands">
|
||||
<CommandsView
|
||||
selectedSerializedNodes={selectedSerializedNodes}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<CustomModeView />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
555
src/components/chat-view/CustomModeView.tsx
Normal file
555
src/components/chat-view/CustomModeView.tsx
Normal file
@ -0,0 +1,555 @@
|
||||
import { Plus, Undo2, Settings, Circle, Trash2 } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
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';
|
||||
const CustomModeView = () => {
|
||||
const {
|
||||
createCustomMode,
|
||||
deleteCustomMode,
|
||||
updateCustomMode,
|
||||
customModeList,
|
||||
} = useCustomModes()
|
||||
|
||||
// 当前选择的模式
|
||||
const [selectedMode, setSelectedMode] = useState<string>('ask')
|
||||
const [isBuiltinMode, setIsBuiltinMode] = useState<boolean>(true)
|
||||
|
||||
|
||||
const isNewMode = React.useMemo(() => selectedMode === "add_new_mode", [selectedMode])
|
||||
|
||||
// new mode config
|
||||
const [newMode, setNewMode] = useState<CustomMode>({
|
||||
id: '',
|
||||
slug: '',
|
||||
name: '',
|
||||
roleDefinition: '',
|
||||
customInstructions: '',
|
||||
groups: [],
|
||||
source: 'global',
|
||||
updatedAt: 0,
|
||||
})
|
||||
|
||||
// custom mode id
|
||||
const [customModeId, setCustomModeId] = useState<string>('')
|
||||
|
||||
// 模型名称
|
||||
const [modeName, setModeName] = useState<string>('')
|
||||
|
||||
// 角色定义
|
||||
const [roleDefinition, setRoleDefinition] = useState<string>('')
|
||||
|
||||
// 选中的工具组
|
||||
const [selectedTools, setSelectedTools] = useState<GroupEntry[]>([])
|
||||
|
||||
// 自定义指令
|
||||
const [customInstructions, setCustomInstructions] = useState<string>('')
|
||||
|
||||
|
||||
// 当模式变更时更新表单数据
|
||||
useEffect(() => {
|
||||
// new mode
|
||||
if (isNewMode) {
|
||||
setIsBuiltinMode(false);
|
||||
setModeName(newMode.name);
|
||||
setRoleDefinition(newMode.roleDefinition);
|
||||
setCustomInstructions(newMode.customInstructions || '');
|
||||
setSelectedTools(newMode.groups as GroupEntry[]);
|
||||
setCustomModeId('');
|
||||
return;
|
||||
}
|
||||
|
||||
const builtinMode = buildinModes.find(m => m.slug === selectedMode);
|
||||
if (builtinMode) {
|
||||
setIsBuiltinMode(true);
|
||||
setModeName(builtinMode.name);
|
||||
setRoleDefinition(builtinMode.roleDefinition);
|
||||
setCustomInstructions(builtinMode.customInstructions || '');
|
||||
setSelectedTools(builtinMode.groups as GroupEntry[]);
|
||||
setCustomModeId(''); // 内置模式没有自定义 ID
|
||||
} else {
|
||||
setIsBuiltinMode(false);
|
||||
const customMode = customModeList.find(m => m.slug === selectedMode);
|
||||
if (customMode) {
|
||||
setCustomModeId(customMode.id || '');
|
||||
setModeName(customMode.name);
|
||||
setRoleDefinition(customMode.roleDefinition);
|
||||
setCustomInstructions(customMode.customInstructions || '');
|
||||
setSelectedTools(customMode.groups);
|
||||
} else {
|
||||
console.log("error, custom mode not found")
|
||||
}
|
||||
}
|
||||
}, [selectedMode, customModeList]);
|
||||
|
||||
|
||||
// 处理工具组选择变更
|
||||
const handleToolChange = React.useCallback((tool: ToolGroup) => {
|
||||
if (isNewMode) {
|
||||
setNewMode((prev) => ({
|
||||
...prev,
|
||||
groups: prev.groups.includes(tool) ? prev.groups.filter(t => t !== tool) : [...prev.groups, tool]
|
||||
}))
|
||||
}
|
||||
setSelectedTools(prev => {
|
||||
if (prev.includes(tool)) {
|
||||
return prev.filter(t => t !== tool);
|
||||
} else {
|
||||
return [...prev, tool];
|
||||
}
|
||||
});
|
||||
}, [isNewMode])
|
||||
|
||||
// 更新模式配置
|
||||
const handleUpdateMode = React.useCallback(async () => {
|
||||
if (!isBuiltinMode) {
|
||||
await updateCustomMode(
|
||||
customModeId,
|
||||
modeName,
|
||||
roleDefinition,
|
||||
customInstructions,
|
||||
selectedTools
|
||||
);
|
||||
}
|
||||
}, [isBuiltinMode, customModeId, modeName, roleDefinition, customInstructions, selectedTools])
|
||||
|
||||
// 创建新模式
|
||||
const createNewMode = React.useCallback(async () => {
|
||||
if (!isNewMode) return;
|
||||
await createCustomMode(
|
||||
modeName,
|
||||
roleDefinition,
|
||||
customInstructions,
|
||||
selectedTools
|
||||
);
|
||||
// reset
|
||||
setNewMode({
|
||||
id: '',
|
||||
slug: '',
|
||||
name: '',
|
||||
roleDefinition: '',
|
||||
customInstructions: '',
|
||||
groups: [],
|
||||
source: 'global',
|
||||
updatedAt: 0,
|
||||
})
|
||||
setSelectedMode("add_new_mode")
|
||||
}, [isNewMode, modeName, roleDefinition, customInstructions, selectedTools])
|
||||
|
||||
// 删除模式
|
||||
const deleteMode = React.useCallback(async () => {
|
||||
if (isNewMode || isBuiltinMode) return;
|
||||
await deleteCustomMode(customModeId);
|
||||
setModeName('')
|
||||
setRoleDefinition('')
|
||||
setCustomInstructions('')
|
||||
setSelectedTools([])
|
||||
setSelectedMode('add_new_mode')
|
||||
}, [isNewMode, isBuiltinMode, customModeId])
|
||||
|
||||
return (
|
||||
<div className="infio-custom-modes-container">
|
||||
{/* 模式配置标题和按钮 */}
|
||||
<div className="infio-custom-modes-header">
|
||||
<div className="infio-custom-modes-title">
|
||||
<h2>模式配置</h2>
|
||||
</div>
|
||||
{/* <div className="infio-custom-modes-actions">
|
||||
<button className="infio-custom-modes-btn">
|
||||
<PlusCircle size={18} />
|
||||
</button>
|
||||
<button className="infio-custom-modes-btn">
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* 创建模式提示 */}
|
||||
<div className="infio-custom-modes-tip">
|
||||
点击 + 创建模式,创建一个新模式。
|
||||
</div>
|
||||
|
||||
{/* 模式选择区 */}
|
||||
<div className="infio-custom-modes-builtin">
|
||||
{[...buildinModes, ...customModeList].map(mode => (
|
||||
<button
|
||||
key={mode.slug}
|
||||
className={`infio-mode-btn ${selectedMode === mode.slug ? 'active' : ''}`}
|
||||
onClick={() => { setSelectedMode(mode.slug) }}
|
||||
>
|
||||
{mode.name}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
key={"add_new_mode"}
|
||||
className={`infio-mode-btn ${selectedMode === "add_new_mode" ? 'active' : ''}`}
|
||||
onClick={() => setSelectedMode("add_new_mode")}
|
||||
>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 模式名称 */}
|
||||
<div className="infio-custom-modes-section">
|
||||
<div className="infio-section-header">
|
||||
<h3>模式名称</h3>
|
||||
{!isBuiltinMode && !isNewMode && (
|
||||
<button className="infio-section-btn" onClick={deleteMode}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={modeName}
|
||||
onChange={(e) => {
|
||||
if (isNewMode) {
|
||||
setNewMode((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
setModeName(e.target.value)
|
||||
}}
|
||||
className="infio-custom-modes-input"
|
||||
placeholder="输入模式名称..."
|
||||
disabled={isBuiltinMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 角色定义 */}
|
||||
<div className="infio-custom-modes-section">
|
||||
<div className="infio-section-header">
|
||||
<h3>角色定义</h3>
|
||||
<button className="infio-section-btn">
|
||||
<Undo2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="infio-section-subtitle">设定专业领域和应答风格</p>
|
||||
<textarea
|
||||
className="infio-custom-textarea"
|
||||
value={roleDefinition}
|
||||
onChange={(e) => {
|
||||
if (isNewMode) {
|
||||
setNewMode((prev) => ({ ...prev, roleDefinition: e.target.value }))
|
||||
}
|
||||
setRoleDefinition(e.target.value)
|
||||
}}
|
||||
placeholder="输入角色定义..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 可用功能 */}
|
||||
<div className="infio-custom-modes-section">
|
||||
<div className="infio-section-header">
|
||||
<h3>可用功能</h3>
|
||||
<button className="infio-section-btn">
|
||||
<Undo2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="infio-section-subtitle">内置模式的可用功能不能被修改</p>
|
||||
<div className="infio-tools-list">
|
||||
<div className="infio-tool-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTools.includes('read')}
|
||||
onChange={() => handleToolChange('read')}
|
||||
/>
|
||||
读取文件
|
||||
</label>
|
||||
</div>
|
||||
<div className="infio-tool-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTools.includes('edit')}
|
||||
onChange={() => handleToolChange('edit')}
|
||||
/>
|
||||
编辑文件
|
||||
</label>
|
||||
</div>
|
||||
<div className="infio-tool-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTools.includes('research')}
|
||||
onChange={() => handleToolChange('research')}
|
||||
/>
|
||||
浏览器
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模式专属规则 */}
|
||||
<div className="infio-custom-modes-section">
|
||||
<div className="infio-section-header">
|
||||
<h3> 模式专属规则(可选)</h3>
|
||||
</div>
|
||||
<p className="infio-section-subtitle">模式专属规则</p>
|
||||
<textarea
|
||||
className="infio-custom-textarea"
|
||||
value={customInstructions}
|
||||
onChange={(e) => {
|
||||
if (isNewMode) {
|
||||
setNewMode((prev) => ({ ...prev, customInstructions: e.target.value }))
|
||||
}
|
||||
setCustomInstructions(e.target.value)
|
||||
}}
|
||||
placeholder="输入模式自定义指令..."
|
||||
/>
|
||||
<p className="infio-section-footer">
|
||||
支持从<a href="#" className="infio-link">_infio_prompts/code-rules/</a>目录读取配置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="infio-custom-modes-actions">
|
||||
<button className="infio-preview-btn">
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
className="infio-preview-btn"
|
||||
onClick={() => {
|
||||
if (isNewMode) {
|
||||
createNewMode()
|
||||
} else {
|
||||
handleUpdateMode()
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 样式 */}
|
||||
<style>
|
||||
{`
|
||||
.infio-custom-modes-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
color: var(--text-normal);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.infio-custom-modes-input {
|
||||
background-color: var(--background-primary) !important;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-normal);
|
||||
padding: var(--size-4-2);
|
||||
font-size: var(--font-ui-small);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.infio-custom-modes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infio-custom-modes-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.infio-custom-modes-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infio-custom-modes-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid #444;
|
||||
color: var(--text-normal)
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-custom-modes-tip {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-custom-modes-builtin {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.infio-mode-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--size-2-2);
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
padding: var(--size-2-3) var(--size-4-3);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-small);
|
||||
align-self: flex-start;
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
|
||||
.infio-mode-btn.active {
|
||||
background-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-custom-modes-custom {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infio-mode-btn-custom {
|
||||
background-color: transparent;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.infio-mode-btn-custom.active {
|
||||
background-color: var(--text-accent);
|
||||
border-color: var(--text-accent);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.infio-custom-modes-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infio-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.infio-section-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.infio-section-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-muted);
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.infio-section-subtitle {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.infio-custom-textarea {
|
||||
background-color: var(--background-primary) !important;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-normal);
|
||||
padding: var(--size-4-2);
|
||||
font-size: var(--font-ui-small);
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.infio-select {
|
||||
width: 100%;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
color: var(--text-normal);
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-tools-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.infio-tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infio-tool-item label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-code-section {
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.infio-code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.infio-section-footer {
|
||||
margin-top: 0px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.infio-link {
|
||||
color: var(--text-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.infio-preview-btn {
|
||||
border: 1px solid #444;
|
||||
color: var(--text-normal);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomModeView
|
||||
@ -33,7 +33,7 @@ import {
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
|
||||
|
||||
export class SystemPromptsManager {
|
||||
export class SystemPrompt {
|
||||
protected dataDir: string
|
||||
protected app: App
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export const ROOT_DIR = '.infio_json_db'
|
||||
export const COMMAND_DIR = 'commands'
|
||||
export const CHAT_DIR = 'chats'
|
||||
export const CUSTOM_MODE_DIR = 'custom_modes'
|
||||
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
|
||||
|
||||
153
src/database/json/custom-mode/CustomModeManager.ts
Executable file
153
src/database/json/custom-mode/CustomModeManager.ts
Executable file
@ -0,0 +1,153 @@
|
||||
import fuzzysort from 'fuzzysort'
|
||||
import { App } from 'obsidian'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { AbstractJsonRepository } from '../base'
|
||||
import { CUSTOM_MODE_DIR, ROOT_DIR } from '../constants'
|
||||
import {
|
||||
DuplicateCustomModeException,
|
||||
EmptyCustomModeNameException,
|
||||
} from '../exception'
|
||||
|
||||
import { CUSTOM_MODE_SCHEMA_VERSION, CustomMode, CustomModeMetadata } from './types'
|
||||
|
||||
export class CustomModeManager extends AbstractJsonRepository<
|
||||
CustomMode,
|
||||
CustomModeMetadata
|
||||
> {
|
||||
constructor(app: App) {
|
||||
super(app, `${ROOT_DIR}/${CUSTOM_MODE_DIR}`)
|
||||
}
|
||||
|
||||
protected generateFileName(mode: CustomMode): string {
|
||||
// Format: v{schemaVersion}_name_id.json (with name encoded)
|
||||
const encodedName = encodeURIComponent(mode.name)
|
||||
return `v${CUSTOM_MODE_SCHEMA_VERSION}_${encodedName}_${mode.id}.json`
|
||||
}
|
||||
|
||||
protected parseFileName(fileName: string): CustomModeMetadata | null {
|
||||
const match = fileName.match(
|
||||
new RegExp(`^v${CUSTOM_MODE_SCHEMA_VERSION}_(.+)_([0-9a-f-]+)\\.json$`),
|
||||
)
|
||||
if (!match) return null
|
||||
|
||||
const encodedName = match[1]
|
||||
const id = match[2]
|
||||
const name = decodeURIComponent(encodedName)
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
updatedAt: Date.now(),
|
||||
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
public async createCustomMode(
|
||||
customMode: Omit<
|
||||
CustomMode,
|
||||
'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'
|
||||
>,
|
||||
): Promise<CustomMode> {
|
||||
if (customMode.name !== undefined && customMode.name.length === 0) {
|
||||
throw new EmptyCustomModeNameException()
|
||||
}
|
||||
|
||||
const existingCustomMode = await this.findByName(customMode.name)
|
||||
if (existingCustomMode) {
|
||||
throw new DuplicateCustomModeException(customMode.name)
|
||||
}
|
||||
|
||||
const newCustomMode: CustomMode = {
|
||||
id: uuidv4(),
|
||||
...customMode,
|
||||
slug: customMode.name.toLowerCase().replace(/ /g, '-'),
|
||||
updatedAt: Date.now(),
|
||||
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
|
||||
}
|
||||
|
||||
await this.create(newCustomMode)
|
||||
return newCustomMode
|
||||
}
|
||||
|
||||
public async ListCustomModes(): Promise<CustomMode[]> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const allCustomModes = await Promise.all(allMetadata.map(async (meta) => this.read(meta.fileName)))
|
||||
return allCustomModes.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
public async findById(id: string): Promise<CustomMode | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.id === id)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async findByName(name: string): Promise<CustomMode | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.name === name)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async updateCustomMode(
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Omit<CustomMode, 'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
|
||||
>,
|
||||
): Promise<CustomMode | null> {
|
||||
if (updates.name !== undefined && updates.name.length === 0) {
|
||||
throw new EmptyCustomModeNameException()
|
||||
}
|
||||
|
||||
const customMode = await this.findById(id)
|
||||
if (!customMode) return null
|
||||
|
||||
if (updates.name && updates.name !== customMode.name) {
|
||||
const existingCustomMode = await this.findByName(updates.name)
|
||||
if (existingCustomMode) {
|
||||
throw new DuplicateCustomModeException(updates.name)
|
||||
}
|
||||
}
|
||||
|
||||
const updatedCustomMode: CustomMode = {
|
||||
...customMode,
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
await this.update(customMode, updatedCustomMode)
|
||||
return updatedCustomMode
|
||||
}
|
||||
|
||||
public async deleteCustomMode(id: string): Promise<boolean> {
|
||||
const customMode = await this.findById(id)
|
||||
if (!customMode) return false
|
||||
|
||||
const fileName = this.generateFileName(customMode)
|
||||
await this.delete(fileName)
|
||||
return true
|
||||
}
|
||||
|
||||
public async searchCustomModes(query: string): Promise<CustomMode[]> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const results = fuzzysort.go(query, allMetadata, {
|
||||
keys: ['name'],
|
||||
threshold: 0.2,
|
||||
limit: 20,
|
||||
all: true,
|
||||
})
|
||||
|
||||
const customModes = (
|
||||
await Promise.all(
|
||||
results.map(async (result) => this.read(result.obj.fileName)),
|
||||
)
|
||||
).filter((customMode): customMode is CustomMode => customMode !== null)
|
||||
|
||||
return customModes
|
||||
}
|
||||
}
|
||||
84
src/database/json/custom-mode/types.ts
Normal file
84
src/database/json/custom-mode/types.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const CUSTOM_MODE_SCHEMA_VERSION = 1
|
||||
|
||||
export const toolGroups = [
|
||||
"read",
|
||||
"edit",
|
||||
"research",
|
||||
// "browser",
|
||||
// "command",
|
||||
// "mcp",
|
||||
"modes",
|
||||
] as const
|
||||
|
||||
export const toolGroupsSchema = z.enum(toolGroups)
|
||||
|
||||
export type ToolGroup = z.infer<typeof toolGroupsSchema>
|
||||
|
||||
export const groupOptionsSchema = z.object({
|
||||
fileRegex: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(pattern) => {
|
||||
if (!pattern) {
|
||||
return true // Optional, so empty is valid.
|
||||
}
|
||||
|
||||
try {
|
||||
new RegExp(pattern)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regular expression pattern" },
|
||||
),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSchema, groupOptionsSchema])])
|
||||
|
||||
export type GroupEntry = z.infer<typeof groupEntrySchema>
|
||||
|
||||
|
||||
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
|
||||
(groups) => {
|
||||
const seen = new Set()
|
||||
|
||||
return groups.every((group) => {
|
||||
// For tuples, check the group name (first element).
|
||||
const groupName = Array.isArray(group) ? group[0] : group
|
||||
|
||||
if (seen.has(groupName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
seen.add(groupName)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{ message: "Duplicate groups are not allowed" },
|
||||
)
|
||||
|
||||
export const modeConfigSchema = z.object({
|
||||
id: z.string().uuid("Invalid ID"),
|
||||
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
updatedAt: z.number().int().positive(),
|
||||
schemaVersion: z.literal(CUSTOM_MODE_SCHEMA_VERSION),
|
||||
})
|
||||
|
||||
export type CustomMode = z.infer<typeof modeConfigSchema>
|
||||
|
||||
export type CustomModeMetadata = {
|
||||
id: string
|
||||
name: string
|
||||
updatedAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
@ -5,6 +5,13 @@ export class DuplicateCommandException extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateCustomModeException extends Error {
|
||||
constructor(customModeName: string) {
|
||||
super(`Custom mode with name "${customModeName}" already exists`)
|
||||
this.name = 'DuplicateCustomModeException'
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyCommandNameException extends Error {
|
||||
constructor() {
|
||||
super('Command name cannot be empty')
|
||||
@ -18,3 +25,10 @@ export class EmptyChatTitleException extends Error {
|
||||
this.name = 'EmptyChatTitleException'
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyCustomModeNameException extends Error {
|
||||
constructor() {
|
||||
super('Custom mode name cannot be empty')
|
||||
this.name = 'EmptyCustomModeNameException'
|
||||
}
|
||||
}
|
||||
95
src/hooks/use-custom-mode.ts
Normal file
95
src/hooks/use-custom-mode.ts
Normal file
@ -0,0 +1,95 @@
|
||||
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'
|
||||
|
||||
|
||||
type UseCustomModes = {
|
||||
createCustomMode: (
|
||||
name: string,
|
||||
roleDefinition: string,
|
||||
customInstructions: string,
|
||||
groups: GroupEntry[]
|
||||
) => Promise<void>
|
||||
deleteCustomMode: (id: string) => Promise<void>
|
||||
updateCustomMode: (
|
||||
id: string,
|
||||
name: string,
|
||||
roleDefinition: string,
|
||||
customInstructions: string,
|
||||
groups: GroupEntry[]
|
||||
) => Promise<void>
|
||||
FindCustomModeByName: (name: string) => Promise<CustomMode | undefined>
|
||||
customModeList: CustomMode[]
|
||||
}
|
||||
|
||||
export function useCustomModes(): UseCustomModes {
|
||||
|
||||
const [customModeList, setCustomModeList] = useState<CustomMode[]>([])
|
||||
|
||||
const app = useApp()
|
||||
const customModeManager = useMemo(() => new CustomModeManager(app), [app])
|
||||
|
||||
const fetchCustomModeList = useCallback(async () => {
|
||||
customModeManager.ListCustomModes().then((rows) => {
|
||||
setCustomModeList(rows)
|
||||
})
|
||||
}, [customModeManager])
|
||||
|
||||
useEffect(() => {
|
||||
void fetchCustomModeList()
|
||||
}, [fetchCustomModeList])
|
||||
|
||||
const createCustomMode = useCallback(
|
||||
async (
|
||||
name: string,
|
||||
roleDefinition: string,
|
||||
customInstructions: string,
|
||||
groups: GroupEntry[]
|
||||
): Promise<void> => {
|
||||
await customModeManager.createCustomMode({
|
||||
name,
|
||||
roleDefinition,
|
||||
customInstructions,
|
||||
groups,
|
||||
})
|
||||
fetchCustomModeList()
|
||||
},
|
||||
[customModeManager, fetchCustomModeList],
|
||||
)
|
||||
|
||||
const deleteCustomMode = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
await customModeManager.deleteCustomMode(id)
|
||||
fetchCustomModeList()
|
||||
},
|
||||
[customModeManager, fetchCustomModeList],
|
||||
)
|
||||
|
||||
const updateCustomMode = useCallback(
|
||||
async (id: string, name: string, roleDefinition: string, customInstructions: string, groups: GroupEntry[]): Promise<void> => {
|
||||
await customModeManager.updateCustomMode(id, {
|
||||
name,
|
||||
roleDefinition,
|
||||
customInstructions,
|
||||
groups,
|
||||
})
|
||||
fetchCustomModeList()
|
||||
},
|
||||
[customModeManager, fetchCustomModeList],
|
||||
)
|
||||
|
||||
const FindCustomModeByName = useCallback(
|
||||
async (name: string): Promise<CustomMode | undefined> => {
|
||||
return customModeList.find((customMode) => customMode.name === name)
|
||||
}, [customModeList])
|
||||
|
||||
return {
|
||||
createCustomMode,
|
||||
deleteCustomMode,
|
||||
updateCustomMode,
|
||||
FindCustomModeByName,
|
||||
customModeList,
|
||||
}
|
||||
}
|
||||
@ -79,13 +79,13 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
|
||||
// Main modes configuration as an ordered array
|
||||
export const modes: readonly ModeConfig[] = [
|
||||
{
|
||||
slug: "research",
|
||||
name: "Research",
|
||||
slug: "ask",
|
||||
name: "Ask",
|
||||
roleDefinition:
|
||||
"You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.",
|
||||
groups: ["research"],
|
||||
"You are Infio, a versatile assistant dedicated to providing informative responses, thoughtful explanations, and practical guidance on virtually any topic or challenge you face.",
|
||||
groups: ["read"],
|
||||
customInstructions:
|
||||
"You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.",
|
||||
"You can analyze information, explain concepts across various domains, and access external resources when helpful. Make sure to address the user's questions thoroughly with thoughtful explanations and practical guidance. Use visual aids like Mermaid diagrams when they help make complex topics clearer. Offer solutions to challenges from diverse fields, not just technical ones, and provide context that helps users better understand the subject matter.",
|
||||
},
|
||||
{
|
||||
slug: "write",
|
||||
@ -97,13 +97,13 @@ export const modes: readonly ModeConfig[] = [
|
||||
"You can create and modify any text-based files, with particular expertise in Markdown formatting. Help users organize their thoughts, create documentation, take notes, or draft any written content they need. When appropriate, suggest structural improvements and formatting enhancements that make content more readable and accessible. Consider the purpose and audience of each document to provide the most relevant assistance."
|
||||
},
|
||||
{
|
||||
slug: "ask",
|
||||
name: "Ask",
|
||||
slug: "research",
|
||||
name: "Research",
|
||||
roleDefinition:
|
||||
"You are Infio, a versatile assistant dedicated to providing informative responses, thoughtful explanations, and practical guidance on virtually any topic or challenge you face.",
|
||||
groups: ["read"],
|
||||
"You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.",
|
||||
groups: ["research"],
|
||||
customInstructions:
|
||||
"You can analyze information, explain concepts across various domains, and access external resources when helpful. Make sure to address the user's questions thoroughly with thoughtful explanations and practical guidance. Use visual aids like Mermaid diagrams when they help make complex topics clearer. Offer solutions to challenges from diverse fields, not just technical ones, and provide context that helps users better understand the subject matter.",
|
||||
"You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.",
|
||||
},
|
||||
] as const
|
||||
|
||||
|
||||
@ -3,12 +3,11 @@ import { App, MarkdownView, TAbstractFile, TFile, TFolder, Vault, getLanguage, h
|
||||
import { editorStateToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||
import { QueryProgressState } from '../components/chat-view/QueryProgress'
|
||||
import { DiffStrategy } from '../core/diff/DiffStrategy'
|
||||
import { SYSTEM_PROMPT } from '../core/prompts/system'
|
||||
import { SystemPrompt } from '../core/prompts/system'
|
||||
import { RAGEngine } from '../core/rag/rag-engine'
|
||||
import { SelectVector } from '../database/schema'
|
||||
import { ChatMessage, ChatUserMessage } from '../types/chat'
|
||||
import { ContentPart, RequestMessage } from '../types/llm/request'
|
||||
import { SystemPromptsManager } from '../core/prompts/system-prompts-manager'
|
||||
import {
|
||||
MentionableBlock,
|
||||
MentionableFile,
|
||||
@ -116,7 +115,7 @@ export class PromptGenerator {
|
||||
private app: App
|
||||
private settings: InfioSettings
|
||||
private diffStrategy: DiffStrategy
|
||||
private systemPromptsManager: SystemPromptsManager
|
||||
private systemPrompt: SystemPrompt
|
||||
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
@ -132,7 +131,7 @@ export class PromptGenerator {
|
||||
this.app = app
|
||||
this.settings = settings
|
||||
this.diffStrategy = diffStrategy
|
||||
this.systemPromptsManager = new SystemPromptsManager(this.app)
|
||||
this.systemPrompt = new SystemPrompt(this.app)
|
||||
}
|
||||
|
||||
public async generateRequestMessages({
|
||||
@ -468,7 +467,7 @@ export class PromptGenerator {
|
||||
}
|
||||
|
||||
private async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise<RequestMessage> {
|
||||
const systemPrompt = await this.systemPromptsManager.getSystemPrompt(
|
||||
const prompt = await this.systemPrompt.getSystemPrompt(
|
||||
this.app.vault.getRoot().path,
|
||||
false,
|
||||
mode,
|
||||
@ -479,7 +478,7 @@ export class PromptGenerator {
|
||||
|
||||
return {
|
||||
role: 'system',
|
||||
content: systemPrompt,
|
||||
content: prompt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user