Skip to content

Commit f849cd9

Browse files
authored
fix: completion config (#490)
* fix: more flexible model message prompt completion config * feat(Electron template): improve scroll
1 parent 30eaa23 commit f849cd9

File tree

8 files changed

+318
-224
lines changed

8 files changed

+318
-224
lines changed

src/evaluator/LlamaChatSession/LlamaChatSession.ts

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -330,18 +330,26 @@ export type LLamaChatCompletePromptOptions = {
330330
enabled?: "auto" | boolean,
331331

332332
/**
333-
* The user prompt to give the model for the completion.
333+
* The messages to append to the chat history to generate a completion as a model response.
334334
*
335-
* Defaults to `"What may I say next?"`
336-
*/
337-
userPrompt?: string,
338-
339-
/**
340-
* The prefix to supply a model message with for the completion.
335+
* If the last message is a model message, the prompt will be pushed to it for the completion,
336+
* otherwise a new model message will be added with the prompt.
341337
*
342-
* Defaults to `"Here's a possible reply from you:\t"`
338+
* It must contain a user message or a system message before the model message.
339+
*
340+
* Default to:
341+
* ```ts
342+
* [
343+
* {
344+
* type: "system",
345+
* text: "For your next response predict what the user may send next. No yapping, no whitespace. Match the user's language and tone."
346+
* },
347+
* {type: "user", text: ""},
348+
* {type: "model", response: [""]}
349+
* ]
350+
* ```
343351
*/
344-
modelPrefix?: string
352+
appendedMessages?: ChatHistoryItem[]
345353
}
346354
};
347355

@@ -391,8 +399,14 @@ export type LlamaChatSessionRepeatPenalty = {
391399

392400
const defaultCompleteAsModel = {
393401
enabled: "auto",
394-
userPrompt: "What may I say next?",
395-
modelPrefix: "Here's a possible reply from you:\t"
402+
appendedMessages: [
403+
{
404+
type: "system",
405+
text: "For your next response predict what the user may send next. No yapping, no whitespace. Match the user's language and tone."
406+
},
407+
{type: "user", text: ""},
408+
{type: "model", response: [""]}
409+
]
396410
} as const satisfies LLamaChatCompletePromptOptions["completeAsModel"];
397411

398412
/**
@@ -928,19 +942,56 @@ export class LlamaChatSession {
928942
throw new DisposedError();
929943

930944
if (shouldCompleteAsModel) {
931-
const completeAsModelUserPrompt = (typeof completeAsModel == "boolean" || completeAsModel === "auto")
932-
? defaultCompleteAsModel.userPrompt
933-
: completeAsModel?.userPrompt ?? defaultCompleteAsModel.userPrompt;
934-
const completeAsModelMessagePrefix = (typeof completeAsModel == "boolean" || completeAsModel === "auto")
935-
? defaultCompleteAsModel.modelPrefix
936-
: completeAsModel?.modelPrefix ?? defaultCompleteAsModel.modelPrefix;
945+
const messagesToAppendOption = (typeof completeAsModel == "boolean" || completeAsModel === "auto")
946+
? defaultCompleteAsModel.appendedMessages
947+
: completeAsModel?.appendedMessages ?? defaultCompleteAsModel.appendedMessages;
948+
949+
const messagesToAppend = messagesToAppendOption.length === 0
950+
? defaultCompleteAsModel.appendedMessages
951+
: messagesToAppendOption;
952+
953+
const addMessageToChatHistory = (chatHistory: ChatHistoryItem[]): {
954+
history: ChatHistoryItem[],
955+
addedCount: number
956+
} => {
957+
const newHistory = chatHistory.slice();
958+
if (messagesToAppend.at(0)?.type === "model")
959+
newHistory.push({type: "user", text: ""});
960+
961+
for (let i = 0; i < messagesToAppend.length; i++) {
962+
const item = messagesToAppend[i];
963+
const isLastItem = i === messagesToAppend.length - 1;
964+
965+
if (item == null)
966+
continue;
967+
968+
if (isLastItem && item.type === "model") {
969+
const newResponse = item.response.slice();
970+
if (typeof newResponse.at(-1) === "string")
971+
newResponse.push((newResponse.pop()! as string) + prompt)
972+
else
973+
newResponse.push(prompt);
974+
975+
newHistory.push({
976+
type: "model",
977+
response: newResponse
978+
})
979+
} else
980+
newHistory.push(item);
981+
}
982+
983+
if (messagesToAppend.at(-1)?.type !== "model")
984+
newHistory.push({type: "model", response: [prompt]});
985+
986+
return {
987+
history: newHistory,
988+
addedCount: newHistory.length - chatHistory.length
989+
};
990+
};
937991

992+
const {history: messagesWithPrompt, addedCount} = addMessageToChatHistory(this._chatHistory);
938993
const {response, lastEvaluation, metadata} = await this._chat.generateResponse(
939-
[
940-
...asWithLastUserMessageRemoved(this._chatHistory),
941-
{type: "user", text: completeAsModelUserPrompt},
942-
{type: "model", response: [completeAsModelMessagePrefix + prompt]}
943-
] as ChatHistoryItem[],
994+
messagesWithPrompt,
944995
{
945996
abortOnNonText: true,
946997
functions,
@@ -968,11 +1019,7 @@ export class LlamaChatSession {
9681019
lastEvaluationContextWindow: {
9691020
history: this._lastEvaluation?.contextWindow == null
9701021
? undefined
971-
: [
972-
...asWithLastUserMessageRemoved(this._lastEvaluation?.contextWindow),
973-
{type: "user", text: completeAsModelUserPrompt},
974-
{type: "model", response: [completeAsModelMessagePrefix + prompt]}
975-
] as ChatHistoryItem[],
1022+
: addMessageToChatHistory(this._lastEvaluation?.contextWindow).history,
9761023
minimumOverlapPercentageToPreventContextShift: 0.8
9771024
}
9781025
}
@@ -981,7 +1028,7 @@ export class LlamaChatSession {
9811028

9821029
this._lastEvaluation = {
9831030
cleanHistory: this._chatHistory,
984-
contextWindow: asWithLastUserMessageRemoved(asWithLastModelMessageRemoved(lastEvaluation.contextWindow)),
1031+
contextWindow: lastEvaluation.contextWindow.slice(0, -addedCount),
9851032
contextShiftMetadata: lastEvaluation.contextShiftMetadata
9861033
};
9871034
this._canUseContextWindowForCompletion = this._chatHistory.at(-1)?.type === "user";
@@ -1183,18 +1230,3 @@ function asWithLastUserMessageRemoved(chatHistory?: ChatHistoryItem[]) {
11831230

11841231
return newChatHistory;
11851232
}
1186-
1187-
1188-
function asWithLastModelMessageRemoved(chatHistory: ChatHistoryItem[]): ChatHistoryItem[];
1189-
function asWithLastModelMessageRemoved(chatHistory: ChatHistoryItem[] | undefined): ChatHistoryItem[] | undefined;
1190-
function asWithLastModelMessageRemoved(chatHistory?: ChatHistoryItem[]) {
1191-
if (chatHistory == null)
1192-
return chatHistory;
1193-
1194-
const newChatHistory = chatHistory.slice();
1195-
1196-
while (newChatHistory.at(-1)?.type === "model")
1197-
newChatHistory.pop();
1198-
1199-
return newChatHistory;
1200-
}

templates/electron-typescript-react/electron/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ function createWindow() {
3030
win = new BrowserWindow({
3131
icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
3232
webPreferences: {
33-
preload: path.join(__dirname, "preload.mjs")
33+
preload: path.join(__dirname, "preload.mjs"),
34+
scrollBounce: true
3435
},
3536
width: 1000,
3637
height: 700
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, {useLayoutEffect, useRef} from "react";
2+
import classNames from "classnames";
3+
4+
export function FixedDivWithSpacer({className, ...props}: FixedDivWithSpacerProps) {
5+
const spacerRef = useRef<HTMLDivElement>(null);
6+
7+
useLayoutEffect(() => {
8+
if (spacerRef.current == null)
9+
return;
10+
11+
const spacerTag = spacerRef.current;
12+
const mainTag = spacerTag.previousElementSibling as HTMLDivElement | null;
13+
14+
if (mainTag == null)
15+
return;
16+
17+
const resizeObserver = new ResizeObserver(() => {
18+
spacerTag.style.width = `${mainTag.offsetWidth}px`;
19+
spacerTag.style.height = `${mainTag.offsetHeight}px`;
20+
});
21+
resizeObserver.observe(mainTag, {
22+
box: "content-box"
23+
});
24+
25+
return () => {
26+
resizeObserver.disconnect();
27+
};
28+
}, [spacerRef]);
29+
30+
return <>
31+
<div className={classNames(className, "main")} {...props} />
32+
<div ref={spacerRef} className={classNames(className, "spacer")} />
33+
</>;
34+
}
35+
36+
type DivProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
37+
type FixedDivWithSpacerProps = DivProps;
Lines changed: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,98 @@
11
.appHeader {
22
display: flex;
33
flex-direction: row;
4-
z-index: 10;
5-
position: sticky;
64
top: 16px;
75
pointer-events: none;
86

9-
> .panel {
10-
pointer-events: all;
11-
display: flex;
12-
flex-direction: row;
13-
align-self: start;
14-
background-color: var(--panel-background-color);
15-
border-radius: 12px;
16-
backdrop-filter: blur(8px);
17-
box-shadow: var(--panel-box-shadow);
18-
overflow: clip;
19-
isolation: isolate;
20-
color: var(--panel-text-color);
7+
&.spacer {
8+
position: sticky;
9+
}
10+
11+
&.main {
12+
width: calc(100% - 16px * 2);
13+
position: fixed;
2114
z-index: 10;
2215

23-
> button {
24-
flex-shrink: 0;
16+
> .panel {
17+
pointer-events: all;
2518
display: flex;
26-
flex-direction: column;
27-
align-items: center;
28-
justify-content: center;
29-
padding: 8px 12px;
30-
margin: 8px;
31-
background-color: var(--panel-button-background-color);
19+
flex-direction: row;
20+
align-self: start;
21+
background-color: var(--panel-background-color);
22+
border-radius: 12px;
23+
backdrop-filter: blur(8px);
24+
box-shadow: var(--panel-box-shadow);
25+
overflow: clip;
26+
isolation: isolate;
3227
color: var(--panel-text-color);
33-
fill: var(--panel-text-color);
28+
z-index: 10;
3429

35-
+ button {
36-
margin-inline-start: 0px;
37-
}
30+
> button {
31+
flex-shrink: 0;
32+
display: flex;
33+
flex-direction: column;
34+
align-items: center;
35+
justify-content: center;
36+
padding: 8px 12px;
37+
margin: 8px;
38+
background-color: var(--panel-button-background-color);
39+
color: var(--panel-text-color);
40+
fill: var(--panel-text-color);
3841

39-
&:hover,
40-
&:focus,
41-
&:focus-visible {
42-
border-color: var(--panel-button-hover-border-color);
43-
}
42+
+ button {
43+
margin-inline-start: 0px;
44+
}
45+
46+
&:hover,
47+
&:focus,
48+
&:focus-visible {
49+
border-color: var(--panel-button-hover-border-color);
50+
}
4451

45-
> .icon {
46-
width: 20px;
47-
height: 20px;
52+
> .icon {
53+
width: 20px;
54+
height: 20px;
55+
}
4856
}
4957
}
50-
}
5158

52-
> .model {
53-
position: relative;
59+
> .model {
60+
position: relative;
5461

55-
> .progress {
56-
position: absolute;
57-
inset-inline-start: 0;
58-
top: 0;
59-
bottom: 0;
60-
background-color: var(--panel-progress-color);
61-
width: calc(var(--progress) * 100%);
62-
pointer-events: none;
63-
z-index: -1;
62+
> .progress {
63+
position: absolute;
64+
inset-inline-start: 0;
65+
top: 0;
66+
bottom: 0;
67+
background-color: var(--panel-progress-color);
68+
width: calc(var(--progress) * 100%);
69+
pointer-events: none;
70+
z-index: -1;
6471

65-
--progress: 0;
72+
--progress: 0;
6673

67-
&.hide {
68-
opacity: 0;
74+
&.hide {
75+
opacity: 0;
6976

70-
transition: opacity 0.3s var(--transition-easing);
77+
transition: opacity 0.3s var(--transition-easing);
78+
}
7179
}
72-
}
7380

74-
> .modelName,
75-
> .noModel {
76-
flex: 1;
77-
text-align: start;
78-
align-self: center;
79-
flex-basis: 400px;
80-
padding: 12px 24px;
81-
word-break: break-word;
81+
> .modelName,
82+
> .noModel {
83+
flex: 1;
84+
text-align: start;
85+
align-self: center;
86+
flex-basis: 400px;
87+
padding: 12px 24px;
88+
word-break: break-word;
8289

83-
margin-inline-end: 48px;
90+
margin-inline-end: 48px;
91+
}
8492
}
85-
}
8693

87-
> .spacer {
88-
flex-grow: 1;
94+
> .spacer {
95+
flex-grow: 1;
96+
}
8997
}
9098
}

templates/electron-typescript-react/src/App/components/Header/Header.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import {CSSProperties} from "react";
22
import classNames from "classnames";
33
import {LoadFileIconSVG} from "../../../icons/LoadFileIconSVG.tsx";
44
import {DeleteIconSVG} from "../../../icons/DeleteIconSVG.tsx";
5-
import {UpdateBadge} from "./components/UpdateBadge.js";
5+
import {FixedDivWithSpacer} from "../FixedDivWithSpacer/FixedDivWithSpacer.tsx";
6+
import {UpdateBadge} from "./components/UpdateBadge.tsx";
67

78
import "./Header.css";
89

910

1011
export function Header({appVersion, canShowCurrentVersion, modelName, onLoadClick, loadPercentage, onResetChatClick}: HeaderProps) {
11-
return <div className="appHeader">
12+
// we use a FixedDivWithSpacer to push down the content while keeping the header fixed.
13+
// this allows the content to have macOS's scroll bounce while keeping the header fixed at the top.
14+
return <FixedDivWithSpacer className="appHeader">
1215
<div className="panel model">
1316
<div
1417
className={classNames("progress", loadPercentage === 1 && "hide")}
@@ -42,7 +45,7 @@ export function Header({appVersion, canShowCurrentVersion, modelName, onLoadClic
4245
appVersion={appVersion}
4346
canShowCurrentVersion={canShowCurrentVersion}
4447
/>
45-
</div>;
48+
</FixedDivWithSpacer>;
4649
}
4750

4851
type HeaderProps = {

0 commit comments

Comments
 (0)