add system preview

This commit is contained in:
duanfuxiang 2025-04-29 11:25:44 +08:00
parent 7791baabaa
commit d1b81dd703
8 changed files with 227 additions and 7 deletions

62
src/PreviewView.tsx Normal file
View File

@ -0,0 +1,62 @@
import { TFile, View, WorkspaceLeaf } from 'obsidian'
import { Root, createRoot } from 'react-dom/client'
import PreviewViewRoot from './components/preview-view/PreviewViewRoot'
import { PREVIEW_VIEW_TYPE } from './constants'
import { AppProvider } from './contexts/AppContext'
export type PreviewViewState = {
content: string
title?: string
file?: TFile
onClose?: () => void
}
export class PreviewView extends View {
private root: Root | null = null
private state: PreviewViewState | null = null
constructor(leaf: WorkspaceLeaf) {
super(leaf)
}
getViewType() {
return PREVIEW_VIEW_TYPE
}
getDisplayText() {
if (this.state?.title) {
return `Preview: ${this.state.title}`
}
return `Preview: ${this.state?.file?.name ?? 'Markdown'}`
}
async setState(state: PreviewViewState) {
this.state = state
// Should render here because onOpen is called before setState
this.render()
}
async onOpen() {
this.root = createRoot(this.containerEl)
}
async onClose() {
if (this.state?.onClose) {
this.state.onClose()
}
this.root?.unmount()
}
async render() {
if (!this.root || !this.state) return
this.root.render(
<AppProvider app={this.app}>
<PreviewViewRoot
state={this.state}
close={() => this.leaf.detach()}
/>
</AppProvider>,
)
}
}

View File

@ -1,11 +1,19 @@
import { ChevronDown, ChevronRight, Plus, Trash2, Undo2 } from 'lucide-react'; import { ChevronDown, ChevronRight, Plus, Trash2, Undo2 } from 'lucide-react';
import React, { useEffect, useState } from 'react'; import { getLanguage } from 'obsidian';
import React, { useEffect, useMemo, useState } from 'react';
import { PREVIEW_VIEW_TYPE } from '../../constants';
import { useApp } from '../../contexts/AppContext'; import { useApp } from '../../contexts/AppContext';
import { useDiffStrategy } from '../../contexts/DiffStrategyContext';
import { useRAG } from '../../contexts/RAGContext';
import { useSettings } from '../../contexts/SettingsContext';
import { CustomMode, GroupEntry, ToolGroup } from '../../database/json/custom-mode/types'; 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 { PreviewView, PreviewViewState } from '../../PreviewView';
import { modes as buildinModes } from '../../utils/modes'; import { modes as buildinModes } from '../../utils/modes';
import { openOrCreateMarkdownFile } from '../../utils/obsidian'; import { openOrCreateMarkdownFile } from '../../utils/obsidian';
import { PromptGenerator, getFullLanguageName } from '../../utils/prompt-generator';
const CustomModeView = () => { const CustomModeView = () => {
const app = useApp() const app = useApp()
@ -14,7 +22,16 @@ const CustomModeView = () => {
deleteCustomMode, deleteCustomMode,
updateCustomMode, updateCustomMode,
customModeList, customModeList,
customModePrompts
} = useCustomModes() } = useCustomModes()
const { settings } = useSettings()
const { getRAGEngine } = useRAG()
const diffStrategy = useDiffStrategy()
const promptGenerator = useMemo(() => {
// @ts-expect-error
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy, customModePrompts, customModeList)
}, [app, settings, diffStrategy, customModePrompts, customModeList])
// 当前选择的模式 // 当前选择的模式
const [selectedMode, setSelectedMode] = useState<string>('ask') const [selectedMode, setSelectedMode] = useState<string>('ask')
@ -326,8 +343,41 @@ const CustomModeView = () => {
</p> </p>
)} )}
</div> </div>
{/* 预览和保存 */}
<div className="infio-custom-modes-actions"> <div className="infio-custom-modes-actions">
<button className="infio-preview-btn"> <button
className="infio-preview-btn"
onClick={async () => {
let filesSearchMethod = settings.filesSearchMethod
if (filesSearchMethod === 'auto' && settings.embeddingModelId && settings.embeddingModelId !== '') {
filesSearchMethod = 'semantic'
}
const userLanguage = getFullLanguageName(getLanguage())
const systemPrompt = await promptGenerator.getSystemMessageNew(modeName, filesSearchMethod, userLanguage)
const existingLeaf = app.workspace
.getLeavesOfType(PREVIEW_VIEW_TYPE)
.find(
(leaf) =>
leaf.view instanceof PreviewView && leaf.view.state.title === `${modeName} system prompt`
)
if (existingLeaf) {
console.log(existingLeaf)
app.workspace.setActiveLeaf(existingLeaf, { focus: true })
} else {
app.workspace.getLeaf(true).setViewState({
type: PREVIEW_VIEW_TYPE,
active: true,
state: {
content: systemPrompt.content as string,
title: `${modeName} system prompt`,
} satisfies PreviewViewState,
})
}
}
}
>
</button> </button>
<button <button

View File

@ -1,6 +1,6 @@
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 React, { useEffect, useState, useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSettings } from '../../../contexts/SettingsContext' import { useSettings } from '../../../contexts/SettingsContext'
import { useCustomModes } from '../../../hooks/use-custom-mode' import { useCustomModes } from '../../../hooks/use-custom-mode'

View File

@ -0,0 +1,105 @@
import { getIcon } from 'obsidian'
import { useEffect, useRef } from 'react'
import { PreviewViewState } from '../../PreviewView'
export default function PreviewViewRoot({
state,
close,
}: {
state: PreviewViewState
close: () => void
}) {
const closeIcon = getIcon('x')
const contentRef = useRef<HTMLDivElement>(null)
// 显示原始文本内容
useEffect(() => {
if (contentRef.current && state.content) {
// 清空现有内容
contentRef.current.empty()
// 创建预格式化文本元素
const preElement = document.createElement('pre')
preElement.className = 'infio-raw-content'
preElement.textContent = state.content
// 添加到容器
contentRef.current.appendChild(preElement)
}
}, [state.content, state.file])
return (
<div id="infio-preview-view">
<div className="view-header">
<div className="view-header-left">
<div className="view-header-nav-buttons"></div>
</div>
<div className="view-header-title-container mod-at-start">
<div className="view-header-title">
{state.title || (state.file ? state.file.name : 'Markdown Preview')}
</div>
<div className="view-actions">
<button
className="clickable-icon view-action infio-close-button"
aria-label="Close preview"
onClick={close}
>
{closeIcon && '✕'}
Close
</button>
</div>
</div>
</div>
<div className="view-content">
<div className="markdown-preview-view is-readable-line-width">
<div className="markdown-preview-sizer">
<div className="infio-preview-title">
{state.title || (state.file ? state.file.name.replace(/\.[^/.]+$/, '') : '')}
</div>
<div
ref={contentRef}
className="markdown-preview-section"
></div>
</div>
</div>
</div>
<style>{`
#infio-preview-view {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--background-primary);
}
#infio-preview-view .view-content {
flex-grow: 1;
overflow: auto;
padding: 0 20px;
}
.infio-preview-title {
font-size: 1.8em;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--background-modifier-border);
}
.markdown-preview-section {
padding: 10px 0;
}
.infio-raw-content {
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-monospace);
padding: 10px;
background-color: var(--background-secondary);
border-radius: 4px;
}
`}</style>
</div>
)
}

View File

@ -2,6 +2,7 @@ import { LLMModel } from './types/llm/model'
// import { ApiProvider } from './utils/api' // import { ApiProvider } from './utils/api'
export const CHAT_VIEW_TYPE = 'infio-chat-view' export const CHAT_VIEW_TYPE = 'infio-chat-view'
export const APPLY_VIEW_TYPE = 'infio-apply-view' export const APPLY_VIEW_TYPE = 'infio-apply-view'
export const PREVIEW_VIEW_TYPE = 'infio-preview-view'
export const DEFAULT_MODELS: LLMModel[] = [] export const DEFAULT_MODELS: LLMModel[] = []

View File

@ -6,13 +6,14 @@ import { Editor, MarkdownView, Notice, Plugin, TFile } from 'obsidian'
import { ApplyView } from './ApplyView' import { ApplyView } from './ApplyView'
import { ChatView } from './ChatView' import { ChatView } from './ChatView'
import { ChatProps } from './components/chat-view/ChatView' import { ChatProps } from './components/chat-view/ChatView'
import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE } from './constants' import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants'
import { getDiffStrategy } from "./core/diff/DiffStrategy" import { getDiffStrategy } from "./core/diff/DiffStrategy"
import { InlineEdit } from './core/edit/inline-edit-processor' import { InlineEdit } from './core/edit/inline-edit-processor'
import { RAGEngine } from './core/rag/rag-engine' import { RAGEngine } from './core/rag/rag-engine'
import { DBManager } from './database/database-manager' import { DBManager } from './database/database-manager'
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase' import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
import EventListener from "./event-listener" import EventListener from "./event-listener"
import { PreviewView } from './PreviewView'
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher" import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
import DocumentChangesListener, { import DocumentChangesListener, {
DocumentChanges, DocumentChanges,
@ -62,6 +63,7 @@ export default class InfioPlugin extends Plugin {
// register views // register views
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this)) this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this))
this.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf)) this.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf))
this.registerView(PREVIEW_VIEW_TYPE, (leaf) => new PreviewView(leaf))
// register markdown processor for Inline Edit // register markdown processor for Inline Edit
this.inlineEdit = new InlineEdit(this, this.settings); this.inlineEdit = new InlineEdit(this, this.settings);

View File

@ -1,7 +1,7 @@
import { App, Editor, MarkdownView, TFile, TFolder, Vault, WorkspaceLeaf } from 'obsidian'
import * as path from 'path' import * as path from 'path'
import { App, Editor, MarkdownView, TFile, TFolder, Vault, WorkspaceLeaf } from 'obsidian'
import { MentionableBlockData } from '../types/mentionable' import { MentionableBlockData } from '../types/mentionable'
export async function readTFileContent( export async function readTFileContent(

View File

@ -472,7 +472,7 @@ export class PromptGenerator {
} }
} }
private async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise<RequestMessage> { public async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise<RequestMessage> {
const prompt = await this.systemPrompt.getSystemPrompt( const prompt = await this.systemPrompt.getSystemPrompt(
this.app.vault.getRoot().path, this.app.vault.getRoot().path,
false, false,