From dcdc95419b7bbd8fd8c6465d16c8c3380380f60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=A6Ltorio?= Date: Wed, 9 Oct 2024 15:48:05 +0200 Subject: [PATCH] Update dependencies: add dompurify package --- package-lock.json | 25 ++++++++ package.json | 2 + src/aipane/aipane.ts | 79 ++++++++++++++++--------- src/aipane/components/TextInsertion.tsx | 40 +++++++++---- 4 files changed, 107 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index d816f6f..6f17511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@sctg/ai-sdk": "0.0.4", "@sctg/sentencepiece-js": "^1.3.3", "core-js": "^3.38.1", + "dompurify": "^3.1.7", "es6-promise": "^4.2.8", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -25,6 +26,7 @@ "@babel/core": "^7.25.7", "@babel/plugin-syntax-import-attributes": "^7.25.7", "@babel/preset-typescript": "^7.25.7", + "@types/dompurify": "^3.0.5", "@types/office-js": "^1.0.434", "@types/office-runtime": "^1.0.35", "@types/react": "^18.3.11", @@ -5830,6 +5832,16 @@ "@types/ms": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -6124,6 +6136,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -9166,6 +9185,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", diff --git a/package.json b/package.json index d8f76a2..b44b762 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@sctg/ai-sdk": "0.0.4", "@sctg/sentencepiece-js": "^1.3.3", "core-js": "^3.38.1", + "dompurify": "^3.1.7", "es6-promise": "^4.2.8", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -44,6 +45,7 @@ "@babel/core": "^7.25.7", "@babel/plugin-syntax-import-attributes": "^7.25.7", "@babel/preset-typescript": "^7.25.7", + "@types/dompurify": "^3.0.5", "@types/office-js": "^1.0.434", "@types/office-runtime": "^1.0.35", "@types/react": "^18.3.11", diff --git a/src/aipane/aipane.ts b/src/aipane/aipane.ts index c98c6ab..201a655 100644 --- a/src/aipane/aipane.ts +++ b/src/aipane/aipane.ts @@ -10,7 +10,8 @@ import { AI } from "@sctg/ai-sdk"; import config from "../config.json" with { type: "json" }; import type { AIAnswer, AIModel, AIPrompt, AIProvider } from "./AIPrompt.js"; import { SentencePieceProcessor, cleanText, llama_3_1_tokeniser_b64 } from "@sctg/sentencepiece-js"; -import { Model, ModelListResponse } from "@sctg/ai-sdk/resources/models.js"; +import { Model } from "@sctg/ai-sdk/resources/models.js"; +import DOMPurify from "dompurify"; const TOKEN_MARGIN: number = 20; // Safety margin for token count const ERROR_MESSAGE: string = "Error: Unable to insert AI answer."; @@ -98,7 +99,13 @@ async function aiRequest( */ function getPrompt(id: string): AIPrompt { const prompts: AIPrompt[] = config.prompts; - return prompts.find((prompt) => prompt.id === id && prompt.standalone !== isOutlookClient()) || prompts[0]; + const prompt: AIPrompt | undefined = prompts.find( + (prompt) => prompt.id === id && prompt.standalone !== isOutlookClient() + ); + if (!prompt) { + throw new Error("Prompt not found"); + } + return prompt; } /** @@ -119,6 +126,12 @@ export async function insertAIAnswer( ): Promise { const { system, user } = getPrompt(id); let error: string | null = ERROR_MESSAGE; + + // Validate and sanitize inputs + if (!system || !user || !userText) { + throw new Error("Invalid input"); + } + try { console.log(`Prompt: ${id}`); console.log(`System text: \n${system}`); @@ -132,11 +145,14 @@ export async function insertAIAnswer( // Replace newlines with HTML line breaks aiText = aiText.replace(/\n/g, "
"); + // Sanitize and escape the AI-generated text + const sanitizedAiText = DOMPurify.sanitize(aiText); + // Insert the AI-generated text into the email body if (isOutlookClient()) { error = null; Office.context.mailbox.item?.body.setSelectedDataAsync( - aiText, + sanitizedAiText, { coercionType: Office.CoercionType.Html }, (asyncResult: Office.AsyncResult) => { if (asyncResult.status === Office.AsyncResultStatus.Failed) { @@ -145,7 +161,7 @@ export async function insertAIAnswer( } ); } - return { response: aiText, error }; + return { response: sanitizedAiText, error }; } catch (err) { console.error("Error: " + err); return { response: "", error }; @@ -166,32 +182,39 @@ export async function getAIModels(provider: AIProvider, apiKey: string, filter: active?: boolean; } - const proxyUrl: string = config.aiproxy.host; - const ai: AI = new AI({ - baseURL: provider.baseUrl, - basePath: provider.basePath, - disableCorsCheck: false, - apiKey, - dangerouslyAllowBrowser: true, - proxy: provider.aiproxied ? proxyUrl : undefined, - }); + try { + const proxyUrl = config.aiproxy.host; + const ai = new AI({ + baseURL: provider.baseUrl, + basePath: provider.basePath, + disableCorsCheck: false, + apiKey, + dangerouslyAllowBrowser: true, + proxy: provider.aiproxied ? proxyUrl : undefined, + }); - const returnedModels: AIModel[] = []; - const models: ModelListResponse = await ai.models.list(); - const filteredModels: ExtendedModel[] = models.data.filter( - (model: ExtendedModel) => model.id.includes(filter) && model.active - ); - const orderedModels: ExtendedModel[] = filteredModels.sort((a, b) => b.created - a.created); - orderedModels.forEach((model) => { - returnedModels.push({ - id: model.id, - name: model.id, - default: false, - max_tokens: model.context_window || 2048, + const models = await ai.models.list(); + const filteredModels = models.data.filter( + (model: ExtendedModel) => model.id.includes(filter) && model.active + ) as ExtendedModel[]; + const orderedModels: ExtendedModel[] = filteredModels.sort((a, b) => b.created - a.created); + + const returnedModels: AIModel[] = []; + orderedModels.forEach((model) => { + returnedModels.push({ + id: model.id, + name: model.id, + default: false, + max_tokens: model.context_window || 2048, + }); }); - }); - returnedModels[0].default = true; - return returnedModels; + returnedModels[0].default = true; + + return returnedModels; + } catch (error) { + console.error("Error retrieving AI models:", error); + throw error; + } } /** diff --git a/src/aipane/components/TextInsertion.tsx b/src/aipane/components/TextInsertion.tsx index 19436f0..4bb84d7 100644 --- a/src/aipane/components/TextInsertion.tsx +++ b/src/aipane/components/TextInsertion.tsx @@ -7,8 +7,8 @@ */ import * as React from "react"; -import { useRef, useState } from "react"; -import { Button, Field, Text, Textarea, tokens, makeStyles } from "@fluentui/react-components"; +import { useState } from "react"; +import { Button, Field, Textarea, tokens, makeStyles, Skeleton, SkeletonItem } from "@fluentui/react-components"; import { AIAnswer } from "../AIPrompt"; import Markdown from "react-markdown"; import rehypeHighlight from "rehype-highlight"; @@ -53,10 +53,19 @@ const useStyles = makeStyles({ textAreaBox: { height: "27vh", }, - text: { - width: "100%", - whiteSpace: "pre-wrap", - overflowWrap: "break-word", + skeleton: { + display: "inherit", + width: "50vw", + }, + skeletonOff: { + display: "none", + }, + skeletonItem: { + margin: "0.5em", + }, + markdown: { + display: "block", + maxWidth: "1024px", }, }); @@ -66,16 +75,18 @@ const useStyles = makeStyles({ * @returns {React.JSX.Element} - The rendered component. */ const TextInsertion: React.FC = (props: TextInsertionProps): React.JSX.Element => { - const textRef = useRef(null); const [text, setText] = useState(props.basePrompt || ""); + const [skeletonVisibility, setSkeletonVisibility] = useState(false); const [answer, setAnswer] = useState(null); /** * Handles the insertion of AI-generated text. */ const handleTextInsertion = async () => { + setSkeletonVisibility(true); const answer = await props.insertAIAnswer(text); - if (answer.error && textRef.current) { + setSkeletonVisibility(false); + if (answer.error) { //textRef.current.innerHTML = `Error: ${answer.error}
Answer: ${answer.response}`; setAnswer(`${answer.error} \nAnswer: \n${answer.response.replace(//g, "\n").replace(/
/g, "\n")}`); } @@ -100,10 +111,17 @@ const TextInsertion: React.FC = (props: TextInsertionProps): - {answer} - +
+ + + + + + + {answer} +   - +
); };