feat: update custom mode draft

This commit is contained in:
duanfuxiang
2025-04-28 16:58:29 +08:00
parent 5558c96aa1
commit 497a9739d7
12 changed files with 2539 additions and 124 deletions

View File

@@ -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>
)

View 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