From ebc4e8305617c5fa5e12ce86fe2fd097277a0bee Mon Sep 17 00:00:00 2001 From: "Gilad S." <7817232+giladgd@users.noreply.github.com> Date: Sun, 22 Sep 2024 02:44:26 +0300 Subject: [PATCH] feat: `resetChatHistory` function on a `LlamaChatSession` (#327) * feat: `resetChatHistory` function on a `LlamaChatSession` * feat: copy model response in the example Electron app template --- .vitepress/utils/parseCmakeListsTxtOptions.ts | 14 ++++++- docs/guide/cmakeOptions.data.ts | 5 ++- src/cli/recommendedModels.ts | 39 +++++++++++++++++ .../LlamaChatSession/LlamaChatSession.ts | 22 +++++++++- .../components/ChatHistory/ChatHistory.css | 31 ++++++++++++++ .../components/ChatHistory/ChatHistory.tsx | 14 +++++-- .../MessageCopyButton/MessageCopyButton.css | 42 +++++++++++++++++++ .../MessageCopyButton/MessageCopyButton.tsx | 38 +++++++++++++++++ .../src/icons/CheckIconSVG.tsx | 7 ++++ .../src/icons/CopyIconSVG.tsx | 7 ++++ 10 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.css create mode 100644 templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.tsx create mode 100644 templates/electron-typescript-react/src/icons/CheckIconSVG.tsx create mode 100644 templates/electron-typescript-react/src/icons/CopyIconSVG.tsx diff --git a/.vitepress/utils/parseCmakeListsTxtOptions.ts b/.vitepress/utils/parseCmakeListsTxtOptions.ts index ced22a3e..ef3acad0 100644 --- a/.vitepress/utils/parseCmakeListsTxtOptions.ts +++ b/.vitepress/utils/parseCmakeListsTxtOptions.ts @@ -1,18 +1,28 @@ +const maxLinesSpan = 10; + export function parseCmakeListsTxtOptions(cmakeListsTxtString: string) { const lines = cmakeListsTxtString.split("\n"); return lines .map((line, index) => { - const match = line.match(/^option\([\s\t\n\r]*(?\S+)[\s\t\n\r]+"(?(?:\\"|[^"])*)"[\s\t\n\r]+(?\S+)[\s\t\n\r]*\)/); - if (match == null || match.groups == null) + const match = lines + .slice(index, index + maxLinesSpan) + .join("\n") + .match( + /^option\([\s\t\n\r]*(?\S+)[\s\t\n\r]+"(?(?:\\"|[^"])*)"[\s\t\n\r]+(?\S+)[\s\t\n\r]*\)/ + ); + if (match == null || match.groups == null || match?.index !== 0) return null; + const totalLines = match[0]?.split("\n")?.length ?? 1; + const {key, description, defaultValue} = match.groups; if (key == null) return null; return { lineNumber: index + 1, + totalLines, key, description: description != null ? description.replaceAll('\\"', '"') diff --git a/docs/guide/cmakeOptions.data.ts b/docs/guide/cmakeOptions.data.ts index 6609ad2e..1c0263c2 100644 --- a/docs/guide/cmakeOptions.data.ts +++ b/docs/guide/cmakeOptions.data.ts @@ -45,7 +45,10 @@ function renderCmakeOptionsTable(cmakeOptions: ReturnType { - const url = githubFileUrl + "#L" + option.lineNumber; + let url = githubFileUrl + "#L" + option.lineNumber; + + if (option.totalLines > 1) + url += "-L" + (option.lineNumber + option.totalLines - 1); return [ `` + diff --git a/src/cli/recommendedModels.ts b/src/cli/recommendedModels.ts index 570055e5..3815f11c 100644 --- a/src/cli/recommendedModels.ts +++ b/src/cli/recommendedModels.ts @@ -259,6 +259,45 @@ export const recommendedModels: ModelRecommendation[] = [{ } }] }, */ { + name: "OLMoE 1b 7B MoE", + abilities: ["chat"], + description: "OLMoE models were created by AllenAI, and are fully open source models that utilize a Mixture of Experts architecture" + + "Mixtures of Experts (MoE) is a technique where different models, each skilled in solving a particular kind of problem, work together to the improve the overall performance on complex tasks.\n" + + "This model includes 64 expert models, with a total of 7 billion parameters.\n" + + "This model generates output extremely fast.", + + fileOptions: [{ + huggingFace: { + model: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", + branch: "main", + file: "olmoe-1b-7b-0924-instruct-q8_0.gguf" + } + }, { + huggingFace: { + model: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", + branch: "main", + file: "olmoe-1b-7b-0924-instruct-q6_k.gguf" + } + }, { + huggingFace: { + model: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", + branch: "main", + file: "olmoe-1b-7b-0924-instruct-q5_k_m.gguf" + } + }, { + huggingFace: { + model: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", + branch: "main", + file: "olmoe-1b-7b-0924-instruct-q4_k_s.gguf" + } + }, { + huggingFace: { + model: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", + branch: "main", + file: "olmoe-1b-7b-0924-instruct-q4_k_m.gguf" + } + }] +}, { name: "Gemma 2 9B", abilities: ["chat", "complete"], description: "Gemma models were created by Google and are optimized suited for variety of text generation tasks, " + diff --git a/src/evaluator/LlamaChatSession/LlamaChatSession.ts b/src/evaluator/LlamaChatSession/LlamaChatSession.ts index e4dedd0e..41e2f952 100644 --- a/src/evaluator/LlamaChatSession/LlamaChatSession.ts +++ b/src/evaluator/LlamaChatSession/LlamaChatSession.ts @@ -289,6 +289,8 @@ export class LlamaChatSession { /** @internal */ private readonly _disposeAggregator = new DisposeAggregator(); /** @internal */ private readonly _autoDisposeSequence: boolean; /** @internal */ private readonly _contextShift?: LlamaChatSessionContextShiftOptions; + /** @internal */ private readonly _forceAddSystemPrompt: boolean; + /** @internal */ private readonly _systemPrompt?: string; /** @internal */ private readonly _chatLock = {}; /** @internal */ private _chatHistory: ChatHistoryItem[]; /** @internal */ private _lastEvaluation?: LlamaChatResponse["lastEvaluation"]; @@ -315,6 +317,8 @@ export class LlamaChatSession { throw new DisposedError(); this._contextShift = contextShift; + this._forceAddSystemPrompt = forceAddSystemPrompt; + this._systemPrompt = systemPrompt; this._chat = new LlamaChat({ autoDisposeSequence, @@ -323,8 +327,8 @@ export class LlamaChatSession { }); const chatWrapperSupportsSystemMessages = this._chat.chatWrapper.settings.supportsSystemMessages; - if (chatWrapperSupportsSystemMessages == null || chatWrapperSupportsSystemMessages || forceAddSystemPrompt) - this._chatHistory = this._chat.chatWrapper.generateInitialChatHistory({systemPrompt}); + if (chatWrapperSupportsSystemMessages == null || chatWrapperSupportsSystemMessages || this._forceAddSystemPrompt) + this._chatHistory = this._chat.chatWrapper.generateInitialChatHistory({systemPrompt: this._systemPrompt}); else this._chatHistory = []; @@ -815,6 +819,20 @@ export class LlamaChatSession { this._lastEvaluation = undefined; } + /** Clear the chat history and reset it to the initial state. */ + public resetChatHistory() { + if (this._chat == null || this.disposed) + throw new DisposedError(); + + const chatWrapperSupportsSystemMessages = this._chat.chatWrapper.settings.supportsSystemMessages; + if (chatWrapperSupportsSystemMessages == null || chatWrapperSupportsSystemMessages || this._forceAddSystemPrompt) + this.setChatHistory( + this._chat.chatWrapper.generateInitialChatHistory({systemPrompt: this._systemPrompt}) + ); + else + this.setChatHistory([]); + } + /** @internal */ private _stopAllPreloadAndPromptCompletions() { for (const abortController of this._preloadAndCompleteAbortControllers) diff --git a/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.css b/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.css index 13692370..4a440c35 100644 --- a/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.css +++ b/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.css @@ -17,6 +17,8 @@ margin-inline-end: 12px; color: var(--user-message-text-color); word-break: break-word; + max-width: calc(100% - 48px - 12px); + box-sizing: border-box; &:not(:first-child) { margin-top: 36px; @@ -28,6 +30,12 @@ margin-inline-end: 48px; padding-inline-start: 24px; word-break: break-word; + max-width: calc(100% - 48px); + box-sizing: border-box; + + &:hover + .buttons { + opacity: 1; + } &.active { &:empty:after, @@ -58,6 +66,15 @@ margin-bottom: 0px; } + h2 { + margin: 16px 0px; + padding-top: 24px; + } + + h3 { + margin: 32px 0px 0px 0px; + } + table { display: block; border-style: hidden; @@ -94,6 +111,20 @@ } } } + + > .buttons { + display: flex; + flex-direction: row; + padding: 8px 18px; + opacity: 0; + + transition: opacity 0.1s ease-in-out; + + &:hover, + &:focus-visible { + opacity: 1; + } + } } @keyframes activeModelMessageIndicator { diff --git a/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.tsx b/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.tsx index b0b61706..23d6d7d3 100644 --- a/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.tsx +++ b/templates/electron-typescript-react/src/App/components/ChatHistory/ChatHistory.tsx @@ -1,6 +1,7 @@ import classNames from "classnames"; import {LlmState} from "../../../../electron/state/llmState.ts"; import {MarkdownContent} from "../MarkdownContent/MarkdownContent.js"; +import {MessageCopyButton} from "./components/MessageCopyButton/MessageCopyButton.js"; import "./ChatHistory.css"; @@ -11,9 +12,16 @@ export function ChatHistory({simplifiedChat, generatingResult}: ChatHistoryProps simplifiedChat.map((item, index) => { if (item.type === "model") { const isActive = index === simplifiedChat.length - 1 && generatingResult; - return - {item.message} - ; + return <> + + {item.message} + + { + !isActive &&
+ +
+ } + ; } else if (item.type === "user") return diff --git a/templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.css b/templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.css new file mode 100644 index 00000000..46385f18 --- /dev/null +++ b/templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.css @@ -0,0 +1,42 @@ +.appChatHistory > .buttons { + > .copyButton { + display: grid; + grid-template-areas: "icon"; + padding: 6px; + border: none; + + transition: background-color 0.1s ease-in-out; + + &:not(:hover, :focus-visible) { + background-color: transparent; + } + + &.copied { + > .icon.copy { + opacity: 0; + transition-delay: 0s; + } + + > .icon.check { + opacity: 1; + transition-delay: 0.1s; + } + } + + > .icon { + grid-area: icon; + width: 18px; + height: 18px; + + transition: opacity 0.3s ease-in-out; + + &.copy { + opacity: 1; + transition-delay: 0.1s; + } + &.check { + opacity: 0; + } + } + } +} diff --git a/templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.tsx b/templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.tsx new file mode 100644 index 00000000..af8f84f7 --- /dev/null +++ b/templates/electron-typescript-react/src/App/components/ChatHistory/components/MessageCopyButton/MessageCopyButton.tsx @@ -0,0 +1,38 @@ +import classNames from "classnames"; +import {useCallback, useState} from "react"; +import {CopyIconSVG} from "../../../../../icons/CopyIconSVG.js"; +import {CheckIconSVG} from "../../../../../icons/CheckIconSVG.js"; + +import "./MessageCopyButton.css"; + +const showCopiedTime = 1000 * 2; + +export function MessageCopyButton({text}: MessageCopyButtonProps) { + const [copies, setCopies] = useState(0); + + const onClick = useCallback(() => { + navigator.clipboard.writeText(text) + .then(() => { + setCopies(copies + 1); + + setTimeout(() => { + setCopies(copies - 1); + }, showCopiedTime); + }) + .catch((error) => { + console.error("Failed to copy text to clipboard", error); + }); + }, [text]); + + return ; +} + +type MessageCopyButtonProps = { + text: string +}; diff --git a/templates/electron-typescript-react/src/icons/CheckIconSVG.tsx b/templates/electron-typescript-react/src/icons/CheckIconSVG.tsx new file mode 100644 index 00000000..c8bec703 --- /dev/null +++ b/templates/electron-typescript-react/src/icons/CheckIconSVG.tsx @@ -0,0 +1,7 @@ +import {SVGProps} from "react"; + +export function CheckIconSVG(props: SVGProps) { + return + + ; +} diff --git a/templates/electron-typescript-react/src/icons/CopyIconSVG.tsx b/templates/electron-typescript-react/src/icons/CopyIconSVG.tsx new file mode 100644 index 00000000..650975ee --- /dev/null +++ b/templates/electron-typescript-react/src/icons/CopyIconSVG.tsx @@ -0,0 +1,7 @@ +import {SVGProps} from "react"; + +export function CopyIconSVG(props: SVGProps) { + return + + ; +}