Refactor AI action handling and add AI tools menu
This commit is contained in:
185
src/App.tsx
185
src/App.tsx
@@ -931,7 +931,13 @@ export default function App() {
|
|||||||
setHistoryItems(combined);
|
setHistoryItems(combined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleConvertToMarkdown = useCallback(async () => {
|
const runAiAction = useCallback(
|
||||||
|
async ({
|
||||||
|
promptKey,
|
||||||
|
actionLabel,
|
||||||
|
openPreviewOnSuccess = false,
|
||||||
|
variables = {}
|
||||||
|
}: AiActionRequest) => {
|
||||||
if (!selectedTextId || !hasText || isViewingHistory || isConverting) return;
|
if (!selectedTextId || !hasText || isViewingHistory || isConverting) return;
|
||||||
if (!ollamaModel) {
|
if (!ollamaModel) {
|
||||||
setConfirmState({
|
setConfirmState({
|
||||||
@@ -942,18 +948,39 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedTranslateLanguage = translateLanguage.trim() || DEFAULT_TRANSLATE_LANGUAGE;
|
||||||
|
if (promptKey === "translate" && !resolvedTranslateLanguage) {
|
||||||
|
setConfirmState({
|
||||||
|
title: "Translate",
|
||||||
|
message: "Set a target language in Settings first.",
|
||||||
|
actionLabel: "OK",
|
||||||
|
onConfirm: () => {}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const prompt = (ollamaPrompt || DEFAULT_OLLAMA_PROMPT).trim();
|
const template = (aiPrompts[promptKey] || DEFAULT_AI_PROMPTS[promptKey]).trim();
|
||||||
const sourceTextId = selectedTextId;
|
const sourceTextId = selectedTextId;
|
||||||
const sourceBody = body;
|
const sourceBody = body;
|
||||||
const sourceTitle = title.trim() || DEFAULT_TITLE;
|
const sourceTitle = title.trim() || DEFAULT_TITLE;
|
||||||
const fullPrompt = `${prompt}\n${sourceBody}`;
|
const sourceDraftBaseVersionId = draftBaseVersionId;
|
||||||
|
const fullPrompt = `${applyPromptVariables(template, {
|
||||||
|
language: resolvedTranslateLanguage,
|
||||||
|
...variables
|
||||||
|
})}\n\nDocument:\n${sourceBody}`;
|
||||||
|
|
||||||
setConversionJob({
|
setConversionJob({
|
||||||
|
actionLabel,
|
||||||
|
openPreviewOnSuccess,
|
||||||
sourceTextId,
|
sourceTextId,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
sourceBody,
|
sourceBody,
|
||||||
|
sourceDraftBaseVersionId,
|
||||||
controller
|
controller
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${normalizedOllamaUrl}/api/generate`, {
|
const response = await fetch(`${normalizedOllamaUrl}/api/generate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -969,52 +996,48 @@ export default function App() {
|
|||||||
throw new Error(`Ollama responded with ${response.status}`);
|
throw new Error(`Ollama responded with ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const resultText = typeof data?.response === "string" ? data.response : "";
|
const resultText = typeof data?.response === "string" ? data.response.trim() : "";
|
||||||
if (!resultText) {
|
if (!resultText) {
|
||||||
throw new Error("Ollama returned an empty response.");
|
throw new Error("Ollama returned an empty response.");
|
||||||
}
|
}
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentText = await getText(sourceTextId);
|
|
||||||
const normalizedTitle = currentText?.title?.trim() || sourceTitle;
|
|
||||||
if (controller.signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await saveManualVersion(sourceTextId, normalizedTitle, resultText);
|
|
||||||
const hasLiveEditsOnSource =
|
const hasLiveEditsOnSource =
|
||||||
selectedTextIdRef.current === sourceTextId &&
|
selectedTextIdRef.current === sourceTextId &&
|
||||||
viewingVersionRef.current === null &&
|
viewingVersionRef.current === null &&
|
||||||
bodyRef.current !== sourceBody;
|
bodyRef.current !== sourceBody;
|
||||||
|
|
||||||
|
if (hasLiveEditsOnSource) {
|
||||||
|
setConfirmState({
|
||||||
|
title: "AI edit skipped",
|
||||||
|
message: `${actionLabel} finished, but the source text changed while it was running. The result was not applied.`,
|
||||||
|
actionLabel: "OK",
|
||||||
|
onConfirm: () => {}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await upsertDraft(sourceTextId, resultText, sourceDraftBaseVersionId);
|
||||||
|
|
||||||
const canApplyToVisibleEditor =
|
const canApplyToVisibleEditor =
|
||||||
selectedTextIdRef.current === sourceTextId &&
|
selectedTextIdRef.current === sourceTextId &&
|
||||||
viewingVersionRef.current === null &&
|
viewingVersionRef.current === null;
|
||||||
!hasLiveEditsOnSource;
|
|
||||||
|
|
||||||
if (hasLiveEditsOnSource) {
|
|
||||||
const currentBody = bodyRef.current;
|
|
||||||
await upsertDraft(sourceTextId, currentBody, result.versionId);
|
|
||||||
setHasDraft(true);
|
|
||||||
setLastPersistedBody(currentBody);
|
|
||||||
setLatestManualVersionId(result.versionId);
|
|
||||||
setDraftBaseVersionId(result.versionId);
|
|
||||||
setSelectedHistoryId(`draft:${sourceTextId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canApplyToVisibleEditor) {
|
if (canApplyToVisibleEditor) {
|
||||||
setBody(resultText);
|
setBody(resultText);
|
||||||
setLastPersistedBody(resultText);
|
setLastPersistedBody(resultText);
|
||||||
setLastPersistedTitle(normalizedTitle);
|
setHasDraft(true);
|
||||||
setHasDraft(false);
|
|
||||||
setRestoredDraft(false);
|
setRestoredDraft(false);
|
||||||
setLatestManualVersionId(result.versionId);
|
setDraftBaseVersionId(sourceDraftBaseVersionId);
|
||||||
setDraftBaseVersionId(result.versionId);
|
setSelectedHistoryId(`draft:${sourceTextId}`);
|
||||||
setSelectedHistoryId(result.versionId);
|
|
||||||
setViewingVersion(null);
|
setViewingVersion(null);
|
||||||
historySnapshotRef.current = null;
|
historySnapshotRef.current = null;
|
||||||
|
if (openPreviewOnSuccess) {
|
||||||
setMarkdownPreview(true);
|
setMarkdownPreview(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await refreshTexts();
|
await refreshTexts();
|
||||||
if (selectedTextIdRef.current === sourceTextId && historyOpenRef.current) {
|
if (selectedTextIdRef.current === sourceTextId && historyOpenRef.current) {
|
||||||
@@ -1024,10 +1047,10 @@ export default function App() {
|
|||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error("Failed to convert with Ollama", error);
|
console.error("Failed to run AI edit", error);
|
||||||
setConfirmState({
|
setConfirmState({
|
||||||
title: "Ollama error",
|
title: "Ollama error",
|
||||||
message: error instanceof Error ? error.message : "Conversion failed.",
|
message: error instanceof Error ? error.message : `${actionLabel} failed.`,
|
||||||
actionLabel: "OK",
|
actionLabel: "OK",
|
||||||
onConfirm: () => {}
|
onConfirm: () => {}
|
||||||
});
|
});
|
||||||
@@ -1036,18 +1059,116 @@ export default function App() {
|
|||||||
current?.controller === controller ? null : current
|
current?.controller === controller ? null : current
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
},
|
||||||
|
[
|
||||||
|
aiPrompts,
|
||||||
body,
|
body,
|
||||||
|
draftBaseVersionId,
|
||||||
hasText,
|
hasText,
|
||||||
isConverting,
|
isConverting,
|
||||||
isViewingHistory,
|
isViewingHistory,
|
||||||
normalizedOllamaUrl,
|
normalizedOllamaUrl,
|
||||||
ollamaModel,
|
ollamaModel,
|
||||||
ollamaPrompt,
|
|
||||||
refreshTexts,
|
refreshTexts,
|
||||||
refreshVersions,
|
refreshVersions,
|
||||||
selectedTextId,
|
selectedTextId,
|
||||||
title
|
title,
|
||||||
|
translateLanguage
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConvertToMarkdown = useCallback(() => {
|
||||||
|
runAiAction({
|
||||||
|
promptKey: "markdownConversion",
|
||||||
|
actionLabel: "Markdown Conversion",
|
||||||
|
openPreviewOnSuccess: true
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to convert to Markdown", error);
|
||||||
|
});
|
||||||
|
}, [runAiAction]);
|
||||||
|
|
||||||
|
const handleOpenAiToolsMenu = useCallback(async () => {
|
||||||
|
if (!selectedTextId || !hasText || isViewingHistory || isConverting) return;
|
||||||
|
|
||||||
|
const menu = await Menu.new({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: "Markdown Conversion",
|
||||||
|
action: () => {
|
||||||
|
handleConvertToMarkdown();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Proofread - Correct Spelling",
|
||||||
|
action: () => {
|
||||||
|
runAiAction({
|
||||||
|
promptKey: "proofreadSpelling",
|
||||||
|
actionLabel: "Proofread - Correct Spelling"
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to proofread text", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Summarize",
|
||||||
|
action: () => {
|
||||||
|
runAiAction({
|
||||||
|
promptKey: "summarize",
|
||||||
|
actionLabel: "Summarize"
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to summarize text", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Translate to ${translateLanguage.trim() || DEFAULT_TRANSLATE_LANGUAGE}`,
|
||||||
|
action: () => {
|
||||||
|
runAiAction({
|
||||||
|
promptKey: "translate",
|
||||||
|
actionLabel: `Translate to ${translateLanguage.trim() || DEFAULT_TRANSLATE_LANGUAGE}`
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to translate text", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Change Style",
|
||||||
|
items: changeStylePresets.map((preset) => ({
|
||||||
|
text: preset,
|
||||||
|
action: () => {
|
||||||
|
runAiAction({
|
||||||
|
promptKey: "changeStyle",
|
||||||
|
actionLabel: `Change Style: ${preset}`,
|
||||||
|
variables: { style: preset }
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to change style", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Rewrite",
|
||||||
|
action: () => {
|
||||||
|
runAiAction({
|
||||||
|
promptKey: "rewrite",
|
||||||
|
actionLabel: "Rewrite"
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to rewrite text", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await menu.popup(undefined, getCurrentWindow());
|
||||||
|
}, [
|
||||||
|
changeStylePresets,
|
||||||
|
handleConvertToMarkdown,
|
||||||
|
hasText,
|
||||||
|
isConverting,
|
||||||
|
isViewingHistory,
|
||||||
|
runAiAction,
|
||||||
|
selectedTextId,
|
||||||
|
translateLanguage
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user