mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Some user experience optimizations (#1045)
This commit is contained in:
parent
6828b5b88a
commit
710f7e1aef
5 changed files with 220 additions and 105 deletions
|
|
@ -11,6 +11,7 @@ import { getSystemPrompt } from "@/settings/model";
|
|||
import { ChatMessage } from "@/sharedState";
|
||||
import { ToolManager } from "@/tools/toolManager";
|
||||
import {
|
||||
err2String,
|
||||
extractChatHistory,
|
||||
extractUniqueTitlesFromDocs,
|
||||
extractYoutubeUrl,
|
||||
|
|
@ -94,9 +95,10 @@ abstract class BaseChainRunner implements ChainRunner {
|
|||
addMessage?: (message: ChatMessage) => void,
|
||||
updateCurrentAiMessage?: (message: string) => void
|
||||
) {
|
||||
if (debug) console.error("Error during LLM invocation:", error);
|
||||
const errorData = error?.response?.data?.error || error;
|
||||
const errorCode = errorData?.code || error;
|
||||
const msg = err2String(error);
|
||||
if (debug) console.error("Error during LLM invocation:", msg);
|
||||
const errorData = error?.response?.data?.error || msg;
|
||||
const errorCode = errorData?.code || msg;
|
||||
let errorMessage = "";
|
||||
|
||||
// Check for specific error messages
|
||||
|
|
@ -113,8 +115,23 @@ abstract class BaseChainRunner implements ChainRunner {
|
|||
|
||||
if (addMessage && updateCurrentAiMessage) {
|
||||
updateCurrentAiMessage("");
|
||||
|
||||
// remove langchain troubleshooting URL from error message
|
||||
const ignoreEndIndex = errorMessage.search("Troubleshooting URL");
|
||||
errorMessage = ignoreEndIndex !== -1 ? errorMessage.slice(0, ignoreEndIndex) : errorMessage;
|
||||
|
||||
// add more user guide for invalid API key
|
||||
if (msg.search(/401|invalid|not valid/gi) !== -1) {
|
||||
errorMessage =
|
||||
"Something went wrong. Please check if you have set your API key." +
|
||||
"\nPath: Settings > copilot plugin > Basic Tab > Set Keys" +
|
||||
"\nError Details: " +
|
||||
errorMessage;
|
||||
}
|
||||
|
||||
addMessage({
|
||||
message: errorMessage,
|
||||
isErrorMessage: true,
|
||||
sender: AI_SENDER,
|
||||
isVisible: true,
|
||||
timestamp: formatDateTime(new Date()),
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import { OramaSearchModal } from "@/components/modals/OramaSearchModal";
|
|||
import { RemoveFromIndexModal } from "@/components/modals/RemoveFromIndexModal";
|
||||
import { ToneModal } from "@/components/modals/ToneModal";
|
||||
import { CustomPromptProcessor } from "@/customPromptProcessor";
|
||||
import { CustomError } from "@/error";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { getAllQAMarkdownContent } from "@/search/searchUtils";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { formatDateTime } from "@/utils";
|
||||
import { formatDateTime, err2String } from "@/utils";
|
||||
import { Editor, Notice, TFile } from "obsidian";
|
||||
import {
|
||||
COMMAND_IDS,
|
||||
|
|
@ -229,8 +228,9 @@ export function registerBuiltInCommands(plugin: CopilotPlugin) {
|
|||
await promptProcessor.savePrompt(title, prompt);
|
||||
new Notice("Custom prompt saved successfully.");
|
||||
} catch (e) {
|
||||
new Notice("Error saving custom prompt. Please check if the title already exists.");
|
||||
console.error(e);
|
||||
const msg = "An error occurred while saving the custom prompt: " + err2String(e);
|
||||
console.error(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}).open();
|
||||
});
|
||||
|
|
@ -318,12 +318,10 @@ export function registerBuiltInCommands(plugin: CopilotPlugin) {
|
|||
await promptProcessor.updatePrompt(promptTitle, title, newPrompt);
|
||||
new Notice(`Prompt "${title}" has been updated.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err instanceof CustomError) {
|
||||
new Notice(err.message);
|
||||
} else {
|
||||
new Notice("An error occurred.");
|
||||
}
|
||||
const msg =
|
||||
"An error occurred while updating the custom prompt: " + err2String(err);
|
||||
console.error(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
},
|
||||
prompt.title,
|
||||
|
|
|
|||
|
|
@ -207,7 +207,10 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
{message.message}
|
||||
</div>
|
||||
) : (
|
||||
<div ref={contentRef}></div>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={message.isErrorMessage ? "text-error" : ""}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -244,7 +247,7 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
{message.message}
|
||||
</div>
|
||||
) : (
|
||||
<div ref={contentRef}></div>
|
||||
<div ref={contentRef} className={message.isErrorMessage ? "text-error" : ""}></div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,104 +1,200 @@
|
|||
import { App, Modal, Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { err2String } from "@/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface AddPromptModalContentProps {
|
||||
initialTitle?: string;
|
||||
initialPrompt?: string;
|
||||
disabledTitle?: boolean;
|
||||
onSave: (title: string, prompt: string) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function AddPromptModalContent({
|
||||
initialTitle = "",
|
||||
initialPrompt = "",
|
||||
disabledTitle = false,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: AddPromptModalContentProps) {
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const [prompt, setPrompt] = useState(initialPrompt);
|
||||
const [touched, setTouched] = useState({ title: false, prompt: false });
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Invalid characters for filename
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const invalidChars = /[<>:"/\\|?*\x00-\x1F]/g;
|
||||
const hasInvalidChars = title && invalidChars.test(title);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (hasInvalidChars) {
|
||||
new Notice("Title contains invalid characters. Please remove them before saving.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (title && prompt) {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onSave(title, prompt);
|
||||
} catch (e) {
|
||||
new Notice(err2String(e));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
} else {
|
||||
setTouched({ title: true, prompt: true });
|
||||
new Notice("Please fill in both fields: Title and Prompt.");
|
||||
}
|
||||
};
|
||||
|
||||
const showTitleError = touched.title && !title;
|
||||
const showPromptError = touched.prompt && !prompt;
|
||||
const isValid = title.trim() !== "" && prompt.trim() !== "" && !hasInvalidChars;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="text-xl font-bold text-normal mb-2">User Custom Prompt</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-base font-medium text-normal">Title</div>
|
||||
<span className="text-error">*</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-sm text-muted">The title of the prompt, must be unique.</div>
|
||||
<div className="text-xs text-warning">
|
||||
Note: Title will be used as filename. Avoid using: {'< > : " / \\ | ? *'}
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
if (!touched.title) setTouched((prev) => ({ ...prev, title: true }));
|
||||
}}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, title: true }))}
|
||||
disabled={disabledTitle}
|
||||
className={`w-full mt-1`}
|
||||
required
|
||||
/>
|
||||
{showTitleError && <div className="text-error text-xs mt-1">Title is required</div>}
|
||||
{hasInvalidChars && (
|
||||
<div className="text-error text-xs mt-1">Title contains invalid characters</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-base font-medium text-normal">Prompt</div>
|
||||
<span className="text-error">*</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted -mt-1">Use the following syntax in your prompt:</div>
|
||||
</div>
|
||||
<div className="text-sm flex flex-col gap-1 bg-secondary/30 rounded-md p-2">
|
||||
<strong>- {"{}"} represents the selected text (not required). </strong>
|
||||
<strong>- {`{[[Note Title]]}`} represents a note. </strong>
|
||||
<strong>- {`{activeNote}`} represents the active note. </strong>
|
||||
<strong>- {`{FolderPath}`} represents a folder of notes. </strong>
|
||||
<strong>
|
||||
- {`{#tag1, #tag2}`} represents ALL notes with ANY of the specified tags in their
|
||||
property (an OR operation).{" "}
|
||||
</strong>
|
||||
<div className="mt-1">
|
||||
<span className="text-muted">
|
||||
Tip: turn on debug mode to show the processed prompt in the chat window.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
value={prompt}
|
||||
onChange={(e) => {
|
||||
setPrompt(e.target.value);
|
||||
if (!touched.prompt) setTouched((prev) => ({ ...prev, prompt: true }));
|
||||
}}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, prompt: true }))}
|
||||
className={`!min-h-[8rem] mt-1`}
|
||||
required
|
||||
/>
|
||||
{showPromptError && <div className="text-error text-xs mt-1">Prompt is required</div>}
|
||||
|
||||
<div className="flex flex-col text-xs text-muted gap-2 mt-2">
|
||||
<div>
|
||||
Save the prompt to the local prompt library. You can then use it with the Copilot
|
||||
command: <strong>Apply custom prompt to selection.</strong>
|
||||
</div>
|
||||
<div>
|
||||
Check out the{" "}
|
||||
<a
|
||||
href="https://github.com/f/awesome-chatgpt-prompts"
|
||||
target="_blank"
|
||||
className="text-accent hover:text-accent-hover"
|
||||
rel="noreferrer"
|
||||
>
|
||||
awesome chatGPT prompts
|
||||
</a>{" "}
|
||||
for inspiration.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!isValid || isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class AddPromptModal extends Modal {
|
||||
private root: Root;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
onSave: (title: string, prompt: string) => void,
|
||||
initialTitle = "",
|
||||
initialPrompt = "",
|
||||
disabledTitle?: boolean
|
||||
private onSave: (title: string, prompt: string) => Promise<void>,
|
||||
private initialTitle = "",
|
||||
private initialPrompt = "",
|
||||
private disabledTitle?: boolean
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
this.contentEl.createEl("h2", { text: "User Custom Prompt" });
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.root = createRoot(contentEl);
|
||||
|
||||
const formContainer = this.contentEl.createEl("div", { cls: "copilot-command-modal" });
|
||||
const handleSave = async (title: string, prompt: string) => {
|
||||
await this.onSave(title, prompt);
|
||||
this.close();
|
||||
};
|
||||
|
||||
const titleContainer = formContainer.createEl("div", {
|
||||
cls: "copilot-command-input-container",
|
||||
});
|
||||
const handleCancel = () => {
|
||||
this.close();
|
||||
};
|
||||
|
||||
titleContainer.createEl("h3", { text: "Title", cls: "copilot-command-header" });
|
||||
titleContainer.createEl("p", {
|
||||
text: "The title of the prompt, must be unique.",
|
||||
cls: "copilot-command-input-description",
|
||||
});
|
||||
this.root.render(
|
||||
<AddPromptModalContent
|
||||
initialTitle={this.initialTitle}
|
||||
initialPrompt={this.initialPrompt}
|
||||
disabledTitle={this.disabledTitle}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const titleField = titleContainer.createEl("input", { type: "text" });
|
||||
if (disabledTitle) {
|
||||
titleField.setAttribute("disabled", "true");
|
||||
}
|
||||
if (initialTitle) {
|
||||
titleField.value = initialTitle;
|
||||
}
|
||||
|
||||
const promptContainer = formContainer.createEl("div", {
|
||||
cls: "copilot-command-input-container",
|
||||
});
|
||||
|
||||
promptContainer.createEl("h3", { text: "Prompt", cls: "copilot-command-header" });
|
||||
|
||||
const promptDescFragment = createFragment((frag) => {
|
||||
frag.createEl("strong", { text: "- {} represents the selected text (not required). " });
|
||||
frag.createEl("br");
|
||||
frag.createEl("strong", { text: "- {[[Note Title]]} represents a note. " });
|
||||
frag.createEl("br");
|
||||
frag.createEl("strong", { text: "- {activeNote} represents the active note. " });
|
||||
frag.createEl("br");
|
||||
frag.createEl("strong", { text: "- {FolderPath} represents a folder of notes. " });
|
||||
frag.createEl("br");
|
||||
frag.createEl("strong", {
|
||||
text: "- {#tag1, #tag2} represents ALL notes with ANY of the specified tags in their property (an OR operation). ",
|
||||
});
|
||||
frag.createEl("br");
|
||||
frag.createEl("br");
|
||||
frag.appendText("Tip: turn on debug mode to show the processed prompt in the chat window.");
|
||||
frag.createEl("br");
|
||||
frag.createEl("br");
|
||||
});
|
||||
promptContainer.appendChild(promptDescFragment);
|
||||
|
||||
const promptField = promptContainer.createEl("textarea");
|
||||
if (initialPrompt) {
|
||||
promptField.value = initialPrompt;
|
||||
}
|
||||
|
||||
const descFragment = createFragment((frag) => {
|
||||
frag.appendText(
|
||||
"Save the prompt to the local prompt library. You can then use it with the Copilot command: "
|
||||
);
|
||||
frag.createEl("strong", { text: "Apply custom prompt to selection." });
|
||||
frag.createEl("br");
|
||||
frag.appendText("Check out the ");
|
||||
frag
|
||||
.createEl("a", {
|
||||
href: "https://github.com/f/awesome-chatgpt-prompts",
|
||||
text: "awesome chatGPT prompts",
|
||||
})
|
||||
.setAttr("target", "_blank");
|
||||
frag.appendText(" for inspiration.");
|
||||
});
|
||||
|
||||
const descContainer = promptContainer.createEl("p", {
|
||||
cls: "copilot-command-input-description",
|
||||
});
|
||||
|
||||
descContainer.appendChild(descFragment);
|
||||
|
||||
const saveButtonContainer = formContainer.createEl("div", {
|
||||
cls: "copilot-command-save-btn-container",
|
||||
});
|
||||
const saveButton = saveButtonContainer.createEl("button", {
|
||||
text: "Save",
|
||||
cls: "copilot-command-save-btn",
|
||||
});
|
||||
saveButton.addEventListener("click", () => {
|
||||
if (titleField.value && promptField.value) {
|
||||
onSave(titleField.value, promptField.value);
|
||||
this.close();
|
||||
} else {
|
||||
new Notice("Please fill in both fields: Title and Prompt.");
|
||||
}
|
||||
});
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface ChatMessage {
|
|||
notes: TFile[];
|
||||
urls: string[];
|
||||
};
|
||||
isErrorMessage?: boolean;
|
||||
}
|
||||
|
||||
class SharedState {
|
||||
|
|
|
|||
Loading…
Reference in a new issue