Some user experience optimizations (#1045)

This commit is contained in:
Emt-lin 2025-02-06 13:56:23 +08:00 committed by GitHub
parent 6828b5b88a
commit 710f7e1aef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 220 additions and 105 deletions

View file

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

View file

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

View file

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

View file

@ -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();
}
}

View file

@ -14,6 +14,7 @@ export interface ChatMessage {
notes: TFile[];
urls: string[];
};
isErrorMessage?: boolean;
}
class SharedState {