-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PopClip extension for revising, polishing, and translating texts, pow…
…ered by ChatGPT. Signed-off-by: Xiaochao Dong (@damnever) <[email protected]>
- Loading branch information
0 parents
commit 286156b
Showing
4 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
{ | ||
"name": "ChatGPTx", | ||
"icon": "iconify:ri:openai-fill", | ||
"icon options": { | ||
"preserve color": false | ||
}, | ||
"identifier": "com.damnever.popclipext.ChatGPTx", | ||
"description": "PopClip extension for revising, polishing, and translating texts, powered by ChatGPT. The output will be pasted/appended or previewed.", | ||
"popclip version": 4096, | ||
"javascript file": "main.js", | ||
"options": [ | ||
{ | ||
"identifier": "apiType", | ||
"label": "API Type", | ||
"type": "multiple", | ||
"description": "The API type.", | ||
"default value": "openai", | ||
"values": [ | ||
"openai", | ||
"azure" | ||
] | ||
}, | ||
{ | ||
"identifier": "apiBase", | ||
"label": "API Base", | ||
"type": "string", | ||
"description": "The API base URL, azure ref: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions.", | ||
"default value": "https://api.openai.com/v1" | ||
}, | ||
{ | ||
"identifier": "apiKey", | ||
"label": "API Key", | ||
"type": "string", | ||
"description": "The API Key for /chat/completions.", | ||
}, | ||
{ | ||
"identifier": "apiVersion", | ||
"label": "API Version, azure only", | ||
"type": "string", | ||
"description": "The API version for /chat/completions.", | ||
"default value": "2023-05-15" | ||
}, | ||
{ | ||
"identifier": "model", | ||
"label": "Model", | ||
"type": "string", | ||
"description": "The model, such as gpt-35-turbo or something else.", | ||
"default value": "gpt-3.5-turbo" | ||
}, | ||
{ | ||
"identifier": "revise", | ||
"label": "Revise", | ||
"type": "boolean", | ||
"description": "Revise revises the text.", | ||
"inset": true | ||
}, | ||
{ | ||
"identifier": "polish", | ||
"label": "Polish", | ||
"type": "boolean", | ||
"description": "Polish correct the grammar and polish the text.", | ||
"inset": true | ||
}, | ||
{ | ||
"identifier": "translate", | ||
"label": "Translate", | ||
"type": "boolean", | ||
"description": "Translate translates the text into Chinese by default.", | ||
"inset": true | ||
}, | ||
{ | ||
"identifier": "prompts", | ||
"label": "Custom Prompts", | ||
"type": "string", | ||
"description": "Customize prompts for revising, polishing, or translating texts. Separate each action on a separate line, for example: [revise] Revise the text.\n[polish] Polish the text.", | ||
"default value": "", | ||
} | ||
], | ||
"entitlements": [ | ||
"network" | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
build: | ||
rm -f ./main.js ChatGPTx.popclipextz | ||
tsc main.ts || exit 0 | ||
pushd .. && zip -r ChatGPTx.popclipextz ChatGPTx.popclipext && mv ChatGPTx.popclipextz ChatGPTx.popclipext && popd |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// @ts-nocheck | ||
import axios from "axios"; | ||
|
||
// TODO: history??? | ||
|
||
interface Message { | ||
role: "user" | "system" | "assistant" | ||
content: string | ||
} | ||
|
||
interface Response { | ||
data: { | ||
choices: [{ | ||
message: Message | ||
}]; | ||
} | ||
} | ||
|
||
// Doc: https://pilotmoon.github.io/PopClip-Extensions/interfaces/Input.html | ||
// Source: https://github.com/pilotmoon/PopClip-Extensions/blob/master/popclip.d.ts | ||
interface Input { | ||
// content: PasteboardContent | ||
// data: { emails: RangedStrings; nonHttpUrls: RangedStrings; paths: RangedStrings; urls: RangedStrings } | ||
html: string | ||
markdown: string | ||
matchedText: string | ||
rtf: string | ||
text: string | ||
xhtml: string | ||
} | ||
|
||
interface Options { | ||
apiType: "openai" | "azure" | ||
apiBase: "string" | ||
apiKey: string | ||
apiVersion: string | ||
model: string | ||
revise: boolean | ||
polish: boolean | ||
translate: boolean | ||
prompts: string | ||
} | ||
|
||
type AllowedActions = "revise" | "polish" | "translate" | ||
|
||
const defaultPrompts: ReadonlyMap<string, string> = new Map(Object.entries({ | ||
"revise": "Please revise the text to make it clearer, more concise, and more coherent, and please list the changes and briefly explain why (NOTE: do not translate the content).", | ||
"polish": "Please correct the grammar and polish the text while adhering as closely as possible to the original intent (NOTE: do not translate the content).", | ||
"translate": "Please translate the text into Chinese and only provide me with the translated content.", | ||
})) | ||
|
||
function getPrompt(action: AllowedActions, customPrompts: string): string { | ||
if (customPrompts !== "") { | ||
const prompts = customPrompts.split("\n") | ||
for (let i = 0; i < prompts.length; i++) { | ||
const parts = prompts[i].trim().split("]") | ||
if (parts[0].substring(1) == action) { | ||
return parts.slice(1).join("]") | ||
} | ||
} | ||
} | ||
return defaultPrompts.get(action) || "" | ||
} | ||
|
||
function constructClientOptions(options: Options): object { | ||
if (options.apiType === "openai") { | ||
return { | ||
"baseURL": options.apiBase, | ||
headers: { Authorization: `Bearer ${options.apiKey}` }, | ||
timeout: 10000, | ||
} | ||
} else if (options.apiType === "azure") { | ||
// Ref: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions | ||
return { | ||
"baseURL": options.apiBase, | ||
headers: { "api-key": `${options.apiKey}` }, | ||
params: { | ||
"api-version": options.apiVersion, | ||
}, | ||
timeout: 10000, | ||
} | ||
} | ||
throw new Error(`unsupported api type: ${options.apiType}`); | ||
} | ||
|
||
const chat = async (input: Input, options: Options, action: AllowedActions) => { | ||
if (!options[action]) { | ||
popclip.showText(`action disabled: ${action}`); | ||
return | ||
} | ||
const prompt = getPrompt(action, options.prompts) | ||
|
||
const openai = axios.create(constructClientOptions(options)) | ||
try { | ||
const { data }: Response = await openai.post( | ||
"chat/completions", | ||
{ | ||
model: options.model, | ||
messages: [ | ||
// { role: "system", content: "You are a professional multilingual assistant who will help me revise, polish, or translate texts. Please strictly follow user instructions." }, | ||
{ | ||
role: "user", content: `${prompt} | ||
The input text being used for this task is enclosed within triple quotation marks below the next line: | ||
"""${input.text}"""` | ||
}, | ||
], | ||
}, | ||
) | ||
const answer = data.choices[0].message.content.trim() | ||
|
||
// Ref: https://pilotmoon.github.io/PopClip-Extensions/interfaces/PopClip.html | ||
if (popclip.context.canPaste) { // Ref: https://pilotmoon.github.io/PopClip-Extensions/interfaces/Context.html | ||
popclip.pasteText(`\n\n${answer}`, { restore: true }) | ||
popclip.showSuccess() | ||
} else { | ||
popclip.showText(answer, { preview: true }) | ||
} | ||
} catch (e) { | ||
popclip.showFailure() | ||
popclip.showText(String(e)) | ||
} | ||
} | ||
|
||
export const actions = [ | ||
{ | ||
title: "ChatGPTx: revise", | ||
// icon: "symbol:square.and.pencil", | ||
icon: "iconify:uil:edit", | ||
requirements: ["option-revise=1"], | ||
code: async (input: Input, options: Options) => chat(input, options, "revise"), | ||
}, | ||
{ | ||
title: "ChatGPTx: polish", | ||
// icon: "symbol:wand.and.stars", | ||
icon: "iconify:lucide:stars", | ||
requirements: ["option-polish=1"], | ||
code: async (input: Input, options: Options) => chat(input, options, "polish"), | ||
}, | ||
{ | ||
title: "ChatGPTx: translate", | ||
// icon: "symbol:rectangle.landscape.rotate", | ||
icon: "iconify:system-uicons:translate", | ||
requirements: ["option-translate=1"], | ||
code: async (input: Input, options: Options) => chat(input, options, "translate"), | ||
}, | ||
] |