Skip to content

Commit

Permalink
PopClip extension for revising, polishing, and translating texts, pow…
Browse files Browse the repository at this point in the history
…ered by ChatGPT.

Signed-off-by: Xiaochao Dong (@damnever) <[email protected]>
  • Loading branch information
damnever committed Aug 5, 2023
0 parents commit 286156b
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## macOS
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

ChatGPTx.popclipextz
main.js
Expand Down
82 changes: 82 additions & 0 deletions Config.json
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"
],
}
4 changes: 4 additions & 0 deletions Makefile
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
147 changes: 147 additions & 0 deletions main.ts
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"),
},
]

0 comments on commit 286156b

Please sign in to comment.