Skip to content

Commit d9f133a

Browse files
committed
feat: improve Electron template
1 parent e49abf2 commit d9f133a

File tree

8 files changed

+116
-62
lines changed

8 files changed

+116
-62
lines changed

templates/electron-typescript-react/electron/state/llmState.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -360,36 +360,46 @@ export const llmFunctions = {
360360
simplifiedChat: getSimplifiedChatHistory(true, message)
361361
}
362362
};
363-
await chatSession.prompt(message, {
364-
signal: promptAbortController.signal,
365-
stopOnAbortSignal: true,
366-
functions: modelFunctions,
367-
onResponseChunk(chunk) {
368-
inProgressResponse = squashMessageIntoModelChatMessages(
369-
inProgressResponse,
370-
(chunk.type == null || chunk.segmentType == null)
371-
? {
372-
type: "text",
373-
text: chunk.text
374-
}
375-
: {
376-
type: "segment",
377-
segmentType: chunk.segmentType,
378-
text: chunk.text,
379-
startTime: chunk.segmentStartTime?.toISOString(),
380-
endTime: chunk.segmentEndTime?.toISOString()
363+
364+
const abortSignal = promptAbortController.signal;
365+
try {
366+
await chatSession.prompt(message, {
367+
signal: abortSignal,
368+
stopOnAbortSignal: true,
369+
functions: modelFunctions,
370+
onResponseChunk(chunk) {
371+
inProgressResponse = squashMessageIntoModelChatMessages(
372+
inProgressResponse,
373+
(chunk.type == null || chunk.segmentType == null)
374+
? {
375+
type: "text",
376+
text: chunk.text
377+
}
378+
: {
379+
type: "segment",
380+
segmentType: chunk.segmentType,
381+
text: chunk.text,
382+
startTime: chunk.segmentStartTime?.toISOString(),
383+
endTime: chunk.segmentEndTime?.toISOString()
384+
}
385+
);
386+
387+
llmState.state = {
388+
...llmState.state,
389+
chatSession: {
390+
...llmState.state.chatSession,
391+
simplifiedChat: getSimplifiedChatHistory(true, message)
381392
}
382-
);
393+
};
394+
}
395+
});
396+
} catch (err) {
397+
if (err !== abortSignal.reason)
398+
throw err;
399+
400+
// if the prompt was aborted before the generation even started, we ignore the error
401+
}
383402

384-
llmState.state = {
385-
...llmState.state,
386-
chatSession: {
387-
...llmState.state.chatSession,
388-
simplifiedChat: getSimplifiedChatHistory(true, message)
389-
}
390-
};
391-
}
392-
});
393403
llmState.state = {
394404
...llmState.state,
395405
chatSession: {
@@ -450,6 +460,8 @@ export const llmFunctions = {
450460
};
451461

452462
chatSession.onDispose.createListener(() => {
463+
chatSessionCompletionEngine = null;
464+
promptAbortController = null;
453465
llmState.state = {
454466
...llmState.state,
455467
chatSession: {

templates/electron-typescript-react/src/App/App.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
min-height: 100%;
1818
max-width: 1280px;
1919

20+
> .chatHistory {
21+
margin-bottom: 32px;
22+
}
23+
2024
> .message {
2125
flex: 1;
2226
display: flex;

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,40 @@ export function App() {
1616
const state = useExternalState(llmState);
1717
const {generatingResult} = state.chatSession;
1818
const isScrollAnchoredRef = useRef(false);
19+
const lastAnchorScrollTopRef = useRef<number>(0);
1920

2021
const isScrolledToTheBottom = useCallback(() => {
21-
return document.documentElement.scrollHeight - document.documentElement.scrollTop === document.documentElement.clientHeight;
22+
return (
23+
document.documentElement.scrollHeight - document.documentElement.scrollTop - 1
24+
) <= document.documentElement.clientHeight;
2225
}, []);
2326

2427
const scrollToBottom = useCallback(() => {
25-
document.documentElement.scrollTop = document.documentElement.scrollHeight;
26-
isScrollAnchoredRef.current = isScrolledToTheBottom();
28+
const newScrollTop = document.documentElement.scrollHeight - document.documentElement.clientHeight;
29+
30+
if (newScrollTop > document.documentElement.scrollTop && newScrollTop > lastAnchorScrollTopRef.current) {
31+
document.documentElement.scrollTo({
32+
top: newScrollTop,
33+
behavior: "smooth"
34+
});
35+
lastAnchorScrollTopRef.current = document.documentElement.scrollTop;
36+
}
37+
38+
isScrollAnchoredRef.current = true;
2739
}, []);
2840

2941
useLayoutEffect(() => {
3042
// anchor scroll to bottom
3143

3244
function onScroll() {
33-
isScrollAnchoredRef.current = isScrolledToTheBottom();
45+
const currentScrollTop = document.documentElement.scrollTop;
46+
47+
isScrollAnchoredRef.current = isScrolledToTheBottom() ||
48+
currentScrollTop >= lastAnchorScrollTopRef.current;
49+
50+
// handle scroll animation
51+
if (isScrollAnchoredRef.current)
52+
lastAnchorScrollTopRef.current = currentScrollTop;
3453
}
3554

3655
const observer = new ResizeObserver(() => {
@@ -169,6 +188,7 @@ export function App() {
169188
{
170189
!showMessage &&
171190
<ChatHistory
191+
className="chatHistory"
172192
simplifiedChat={state.chatSession.simplifiedChat}
173193
generatingResult={generatingResult}
174194
/>

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

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,46 @@
1+
import {useMemo} from "react";
2+
import classNames from "classnames";
13
import {LlmState, SimplifiedModelChatItem} from "../../../../electron/state/llmState.ts";
24
import {UserMessage} from "./components/UserMessage/UserMessage.js";
35
import {ModelMessage} from "./components/ModelMessage/ModelMessage.js";
46

57
import "./ChatHistory.css";
68

79

8-
export function ChatHistory({simplifiedChat, generatingResult}: ChatHistoryProps) {
9-
return <div className="appChatHistory">
10-
{
11-
simplifiedChat.map((item, index) => {
12-
if (item.type === "model")
13-
return <ModelMessage
14-
key={index}
15-
modelMessage={item}
16-
active={index === simplifiedChat.length - 1 && generatingResult}
17-
/>;
18-
else if (item.type === "user")
19-
return <UserMessage key={index} message={item} />;
20-
21-
return null;
22-
})
23-
}
10+
export function ChatHistory({simplifiedChat, generatingResult, className}: ChatHistoryProps) {
11+
const renderChatItems = useMemo(() => {
12+
if (simplifiedChat.length > 0 &&
13+
simplifiedChat.at(-1)!.type !== "model" &&
14+
generatingResult
15+
)
16+
return [...simplifiedChat, emptyModelMessage];
17+
18+
return simplifiedChat;
19+
}, [simplifiedChat, generatingResult]);
20+
21+
return <div className={classNames("appChatHistory", className)}>
2422
{
25-
(
26-
simplifiedChat.length > 0 &&
27-
simplifiedChat[simplifiedChat.length - 1]!.type !== "model" &&
28-
generatingResult
29-
) &&
30-
<ModelMessage modelMessage={emptyModelMessage} active />
23+
renderChatItems
24+
.map((item, index) => {
25+
if (item.type === "model")
26+
return <ModelMessage
27+
key={index}
28+
modelMessage={item}
29+
active={index === renderChatItems.length - 1 && generatingResult}
30+
/>;
31+
else if (item.type === "user")
32+
return <UserMessage key={index} message={item} />;
33+
34+
return null;
35+
})
3136
}
3237
</div>;
3338
}
3439

3540
type ChatHistoryProps = {
3641
simplifiedChat: LlmState["chatSession"]["simplifiedChat"],
37-
generatingResult: boolean
42+
generatingResult: boolean,
43+
className?: string
3844
};
3945

4046
const emptyModelMessage: SimplifiedModelChatItem = {

templates/electron-typescript-react/src/App/components/ChatHistory/components/ModelMessage/ModelMessage.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@
55
word-break: break-word;
66
max-width: calc(100% - 48px);
77
box-sizing: border-box;
8+
min-height: fit-content;
9+
interpolate-size: allow-keywords;
10+
11+
transition: min-height 0.5s var(--transition-easing), max-height 0.5s var(--transition-easing);
812

913
&:hover + .buttons {
1014
opacity: 1;
1115
}
1216

1317
&:last-child {
1418
margin-bottom: 0px;
19+
min-height: calc(50svh);
20+
21+
@starting-style {
22+
min-height: 0px;
23+
}
1524
}
1625

1726
> .text {
@@ -31,5 +40,9 @@
3140
&:focus-visible {
3241
opacity: 1;
3342
}
43+
44+
&[inert] {
45+
opacity: 0;
46+
}
3447
}
3548
}

templates/electron-typescript-react/src/App/components/ChatHistory/components/ModelMessage/ModelMessage.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ export function ModelMessage({modelMessage, active}: ModelMessageProps) {
3737
(modelMessage.message.length === 0 && active) &&
3838
<MessageMarkdown className="text" activeDot />
3939
}
40-
{
41-
!active && <div className="buttons">
42-
<ModelMessageCopyButton modelMessage={modelMessage.message} />
43-
</div>
44-
}
40+
<div className="buttons" inert={active}>
41+
<ModelMessageCopyButton modelMessage={modelMessage.message} />
42+
</div>
4543
</div>;
4644
}
4745

templates/electron-typescript-react/src/App/components/ChatHistory/components/UserMessage/UserMessage.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
padding: 8px 12px;
55
border-radius: 12px;
66
margin-top: 0px;
7-
margin-bottom: 12px;
7+
margin-bottom: 16px;
88
margin-inline-start: 48px;
99
margin-inline-end: 12px;
1010
color: var(--user-message-text-color);

templates/electron-typescript-react/src/App/components/MessageMarkdown/MessageMarkdown.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
&.active {
55
&:empty:after,
66
&:not(:empty)>:last-child:not(ol, ul, table):after,
7-
&:not(:empty)>:last-child:where(ol, ul)>:last-child:after,
7+
&:not(:empty)>:last-child:where(ol, ul)>:last-child:not(:has(>:last-child:where(ol, ul))):after,
8+
&:not(:empty)>:last-child:where(ol, ul)>:last-child>:last-child:where(ol, ul)>:last-child:after,
89
&:not(:empty)>:last-child:where(table)>:last-child>:last-child>:last-child:after {
910
content: "";
1011
position: static;

0 commit comments

Comments
 (0)