This commit is contained in:
duanfuxiang
2025-01-05 11:51:39 +08:00
commit 0c7ee142cb
215 changed files with 20611 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
import { MarkdownView, Plugin } from "obsidian";
import React, { useEffect, useRef, useState } from "react";
import { APPLY_VIEW_TYPE } from "../../constants";
import LLMManager from "../../core/llm/manager";
import { InfioSettings } from "../../types/settings";
import { manualApplyChangesToFile } from "../../utils/apply";
import { removeAITags } from "../../utils/content-filter";
import { PromptGenerator } from "../../utils/prompt-generator";
interface InlineEditProps {
source: string;
secStartLine: number;
secEndLine: number;
plugin: Plugin;
settings: InfioSettings;
}
interface InputAreaProps {
value: string;
onChange: (value: string) => void;
}
const InputArea: React.FC<InputAreaProps> = ({ value, onChange }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
// 组件挂载后自动聚焦到 textarea
textareaRef.current?.focus();
}, []);
return (
<div className="infio-ai-block-input-wrapper">
<textarea
ref={textareaRef}
className="infio-ai-block-content"
placeholder="Enter instruction"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
};
interface ControlAreaProps {
settings: InfioSettings;
onSubmit: () => void;
selectedModel: string;
onModelChange: (model: string) => void;
isSubmitting: boolean;
}
const ControlArea: React.FC<ControlAreaProps> = ({
settings,
onSubmit,
selectedModel,
onModelChange,
isSubmitting,
}) => (
<div className="infio-ai-block-controls">
<select
className="infio-ai-block-model-select"
value={selectedModel}
onChange={(e) => onModelChange(e.target.value)}
disabled={isSubmitting}
>
{settings.activeModels
.filter((model) => !model.isEmbeddingModel && model.enabled)
.map((model) => (
<option key={model.name} value={model.name}>
{model.name}
</option>
))}
</select>
<button
className="infio-ai-block-submit-button"
onClick={onSubmit}
disabled={isSubmitting}
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</div>
);
export const InlineEdit: React.FC<InlineEditProps> = ({
source,
secStartLine,
secEndLine,
plugin,
settings,
}) => {
const [instruction, setInstruction] = useState("");
const [selectedModel, setSelectedModel] = useState(settings.chatModelId);
const [isSubmitting, setIsSubmitting] = useState(false);
const llmManager = new LLMManager({
deepseek: settings.deepseekApiKey,
openai: settings.openAIApiKey,
anthropic: settings.anthropicApiKey,
gemini: settings.geminiApiKey,
groq: settings.groqApiKey,
infio: settings.infioApiKey,
});
const promptGenerator = new PromptGenerator(
async () => {
throw new Error("RAG not needed for inline edit");
},
plugin.app,
settings
);
const handleClose = () => {
const activeView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView?.editor) return;
activeView.editor.replaceRange(
"",
{ line: secStartLine, ch: 0 },
{ line: secEndLine + 1, ch: 0 }
);
};
const getActiveContext = async () => {
const activeFile = plugin.app.workspace.getActiveFile();
if (!activeFile) {
console.error("No active file");
return {};
}
const editor = plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
if (!editor) {
console.error("No active editor");
return { activeFile };
}
const selection = editor.getSelection();
if (!selection) {
console.error("No text selected");
return { activeFile, editor };
}
return { activeFile, editor, selection };
};
const parseSmartComposeBlock = (content: string) => {
const match = content.match(/<infio_block[^>]*>([\s\S]*?)<\/infio_block>/);
if (!match) {
return null;
}
const blockContent = match[1].trim();
const attributes = match[0].match(/startLine="(\d+)"/);
const startLine = attributes ? parseInt(attributes[1]) : undefined;
const endLineMatch = match[0].match(/endLine="(\d+)"/);
const endLine = endLineMatch ? parseInt(endLineMatch[1]) : undefined;
return {
startLine,
endLine,
content: blockContent,
};
};
const handleSubmit = async () => {
setIsSubmitting(true);
try {
const { activeFile, editor, selection } = await getActiveContext();
if (!activeFile || !editor || !selection) {
setIsSubmitting(false);
return;
}
const chatModel = settings.activeModels.find(
(model) => model.name === selectedModel
);
if (!chatModel) {
setIsSubmitting(false);
throw new Error("Invalid chat model");
}
const from = editor.getCursor("from");
const to = editor.getCursor("to");
const defaultStartLine = from.line + 1;
const defaultEndLine = to.line + 1;
const requestMessages = await promptGenerator.generateEditMessages({
currentFile: activeFile,
selectedContent: selection,
instruction: instruction,
startLine: defaultStartLine,
endLine: defaultEndLine,
});
const response = await llmManager.generateResponse(chatModel, {
model: chatModel.name,
messages: requestMessages,
stream: false,
});
if (!response.choices[0].message.content) {
setIsSubmitting(false);
throw new Error("Empty response from LLM");
}
const parsedBlock = parseSmartComposeBlock(
response.choices[0].message.content
);
const finalContent = parsedBlock?.content || response.choices[0].message.content;
const startLine = parsedBlock?.startLine || defaultStartLine;
const endLine = parsedBlock?.endLine || defaultEndLine;
const updatedContent = await manualApplyChangesToFile(
finalContent,
activeFile,
await plugin.app.vault.read(activeFile),
startLine,
endLine
);
if (!updatedContent) {
console.error("Failed to apply changes");
setIsSubmitting(false);
return;
}
const originalContent = await plugin.app.vault.read(activeFile);
await plugin.app.workspace.getLeaf(true).setViewState({
type: APPLY_VIEW_TYPE,
active: true,
state: {
file: activeFile,
originalContent: removeAITags(originalContent),
newContent: removeAITags(updatedContent),
},
});
} catch (error) {
console.error("Error in inline edit:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="infio-ai-block-container"
id="infio-ai-block-container"
style={{ backgroundColor: 'var(--background-secondary)' }}
>
<InputArea value={instruction} onChange={setInstruction} />
<button className="infio-ai-block-close-button" onClick={handleClose}>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<ControlArea
settings={settings}
onSubmit={handleSubmit}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
isSubmitting={isSubmitting}
/>
</div>
);
};