Skip to content

Commit ef7cd84

Browse files
authored
Condensed chat (#16)
* basic draft of condensed chat * labels for chat views * removed unused imports * added comments to condensing function
1 parent 980ae6c commit ef7cd84

File tree

4 files changed

+195
-35
lines changed

4 files changed

+195
-35
lines changed

src/components/Chat/Chat.module.scss

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
height: calc(100%);
88

99
#chat-scroll-container {
10-
height: calc(100% - 35px - 2.5rem);
10+
height: calc(100% - 35px);
1111
overflow-y: auto;
1212
margin-bottom: 3px;
1313
padding: 0.5rem;
14+
padding-top: 1rem;
15+
padding-bottom: 1.5rem;
1416

1517
pre {
1618
white-space: pre-wrap;
@@ -59,6 +61,35 @@
5961
text-align: right;
6062
}
6163
}
64+
65+
.condensed-chat {
66+
background-color: #333;
67+
padding: 0.5rem;
68+
border-radius: 5px;
69+
border: 1px solid #555;
70+
71+
margin-bottom: 1.5rem;
72+
73+
color: white;
74+
position: relative;
75+
76+
> p:first-child {
77+
margin-top: 0;
78+
}
79+
80+
a {
81+
font-size: 0.8rem;
82+
cursor: pointer;
83+
text-decoration: underline;
84+
}
85+
86+
.show-all-content-container {
87+
padding: 0.5rem;
88+
padding-bottom: 0;
89+
border-radius: 5px;
90+
background-color: #222;
91+
}
92+
}
6293
}
6394

6495
.chat-status-badge {
@@ -69,8 +100,10 @@
69100
left: 0;
70101
right: 0;
71102
padding: 0.5rem;
72-
background: #22222280;
73-
background: linear-gradient(0deg, rgba(34,34,34,0.75) 0%, rgba(34,34,34,0) 100%);
103+
104+
:global(.mantine-Badge-root) {
105+
box-shadow: 0 0 3px 3px rgba(0,0,0,0.3);
106+
}
74107
}
75108

76109

src/components/Chat/Chat.tsx

Lines changed: 132 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ import { useAppDispatch, useAppSelector } from 'redux/store';
2222
import { tryParsingOutQuery } from 'utils/tryParsingOutQuery';
2323

2424
import styles from "./Chat.module.scss"
25+
import { LinkQChatMessageType } from 'redux/chatHistorySlice';
2526

2627

2728
export function Chat() {
2829
const fullChatHistory = useAppSelector(state => state.chatHistory.fullChatHistory)
2930
const simpleChatHistory = useAppSelector(state => state.chatHistory.simpleChatHistory)
3031

31-
const showFullChatHistory = useAppSelector(state => state.chatHistory.showFullChatHistory)
32+
const chatHistoryDisplay = useAppSelector(state => state.chatHistory.chatHistoryDisplay)
3233

33-
//based on showFullChatHistory, decide which chat history to display to the user
34-
const chatHistory = showFullChatHistory ? fullChatHistory : simpleChatHistory
34+
//based on chatHistoryDisplay, decide which chat history to display to the user
35+
const chatHistory = chatHistoryDisplay==="full" ? fullChatHistory : simpleChatHistory
3536

3637
const chatScrollBottomRef = useRef<HTMLDivElement>(null)
3738
useEffect(() => {
@@ -55,20 +56,16 @@ export function Chat() {
5556
<Settings/>
5657

5758
<div id={styles["chat-scroll-container"]}>
58-
{chatHistory.map((c, i) => {
59-
return (
60-
<div key={i} className={`${styles["chat-row"]} ${styles[c.role]}`}>
61-
<div className={styles["chat-justify"]}>
62-
{showFullChatHistory && <p>{c.name}, chat #{c.chatId}</p>}
63-
{
64-
c.role === "assistant"
65-
? <RenderLLMResponse text={c.content} setInputText={setInputText}/>
66-
: <pre className={styles.chat}>{c.content}</pre>
67-
}
68-
</div>
69-
</div>
70-
)
71-
})}
59+
<br/>
60+
{chatHistoryDisplay==="condensed" ? (
61+
condenseChat(fullChatHistory).map((c,i) => (
62+
<RenderCondensedMessage key={i} condensedChat={c} setInputText={setInputText}/>
63+
))
64+
) : (
65+
chatHistory.map((c, i) => (
66+
<RenderChatMessage key={i} chat={c} setInputText={setInputText}/>
67+
))
68+
)}
7269
<div ref={chatScrollBottomRef}/>
7370
</div>
7471

@@ -99,6 +96,68 @@ export function Chat() {
9996
)
10097
}
10198

99+
function RenderCondensedMessage({
100+
condensedChat,
101+
setInputText,
102+
}:{
103+
condensedChat:CondensedChatType,
104+
setInputText: React.Dispatch<React.SetStateAction<string>>,
105+
}) {
106+
const [showDetails, setShowDetails] = useState<boolean>(false)
107+
108+
const firstChatMessage = condensedChat[0]
109+
return (
110+
<div className={styles["condensed-chat"]}>
111+
{firstChatMessage.stage && (
112+
<>
113+
<p><b>{firstChatMessage.stage.mainStage}</b></p>
114+
<p>{firstChatMessage.stage.subStage}</p>
115+
{firstChatMessage.stage.description && <p>{firstChatMessage.stage.description}</p>}
116+
</>
117+
)}
118+
<div>
119+
<a aria-label="Show Details" onClick={() => setShowDetails(!showDetails)}>
120+
{showDetails ? "Hide Details" : "Show Full Details"}
121+
</a>
122+
</div>
123+
124+
{showDetails && (
125+
<div>
126+
<br/>
127+
<div className={styles["show-all-content-container"]}>
128+
{condensedChat.map((c, i) => (
129+
<RenderChatMessage key={i} chat={c} setInputText={setInputText}/>
130+
))}
131+
</div>
132+
</div>
133+
)}
134+
</div>
135+
)
136+
}
137+
138+
function RenderChatMessage({
139+
chat,
140+
setInputText,
141+
}:{
142+
chat:LinkQChatMessageType,
143+
setInputText: React.Dispatch<React.SetStateAction<string>>,
144+
}) {
145+
const chatHistoryDisplay = useAppSelector(state => state.chatHistory.chatHistoryDisplay)
146+
147+
return (
148+
<div className={`${styles["chat-row"]} ${styles[chat.role]}`}>
149+
<div className={styles["chat-justify"]}>
150+
{chatHistoryDisplay==="full"||chatHistoryDisplay==="condensed" && <p>{chat.name}, chat #{chat.chatId}</p>}
151+
{
152+
chat.role === "assistant"
153+
? <RenderLLMResponse text={chat.content} setInputText={setInputText}/>
154+
: <pre className={styles.chat}>{chat.content}</pre>
155+
}
156+
</div>
157+
</div>
158+
)
159+
}
160+
102161
function RenderLLMResponse({
103162
setInputText,
104163
text,
@@ -220,6 +279,60 @@ function LinkQDetailedBadgeStatus() {
220279
}
221280

222281
return (
223-
<div className={styles["chat-status-badge"]}><Badge color={color}>{displayMessage}</Badge></div>
282+
<div className={styles["chat-status-badge"]}>
283+
<Badge color={color}>{displayMessage}</Badge>
284+
</div>
224285
)
225-
}
286+
}
287+
288+
//the condensed chat type is just one or two grouped chat messages
289+
type CondensedChatType = LinkQChatMessageType[]
290+
291+
/**
292+
* Converts the full chat history into the condensed/grouped view for better traceability.
293+
* If two neighboring chat messages have the same stage details, they are condensed/grouped together
294+
* @param fullChatHistory
295+
* @returns array of condensed chat types
296+
*/
297+
function condenseChat(fullChatHistory: LinkQChatMessageType[]):CondensedChatType[] {
298+
const condensedChat:CondensedChatType[] = [];
299+
300+
//loop through the full chat history
301+
for(let i=0; i<fullChatHistory.length; ++i) {
302+
const currentChatMessage = fullChatHistory[i]
303+
const nextChatMessage = fullChatHistory.at(i + 1)
304+
if(i === 0) { //HARDCODED ignore the initial system prompt
305+
continue;
306+
}
307+
//else if the current and next chat message have the same stage details
308+
else if(nextChatMessage && messagesHaveMatchingStages(currentChatMessage,nextChatMessage)) {
309+
//condense these two messages together
310+
condensedChat.push([
311+
currentChatMessage,
312+
nextChatMessage
313+
]);
314+
++i; //skip over this next message in the subsequent iteration
315+
}
316+
//else this chat message can stand alone by itself
317+
else if(currentChatMessage.stage) {
318+
condensedChat.push([ currentChatMessage ])
319+
}
320+
//else the chat message doesn't have stage info
321+
}
322+
323+
return condensedChat
324+
}
325+
326+
/**
327+
* Checks whether two messages have the same main and sub stage
328+
* @param chatMessage1
329+
* @param chatMessage2
330+
* @returns true if the main and sub stages match (including in the stage is undefined), else false
331+
*/
332+
function messagesHaveMatchingStages(
333+
chatMessage1: LinkQChatMessageType,
334+
chatMessage2: LinkQChatMessageType,
335+
) {
336+
return chatMessage1.stage?.mainStage===chatMessage2.stage?.mainStage
337+
|| chatMessage1.stage?.subStage===chatMessage2.stage?.subStage
338+
}

src/components/Settings/Settings.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ErrorMessage } from "components/ErrorMessage"
1010
import { useMainChatAPI } from "hooks/useMainChatAPI"
1111

1212
import { setApiKey, setBaseURL, setModel, toggleShowStateDiagramStatus } from "redux/settingsSlice"
13-
import { toggleShowFullChatHistory } from "redux/chatHistorySlice"
13+
import { CHAT_HISTORY_DISPLAY_OPTIONS, ChatHistoryDisplayType, setChatHistoryDisplay } from "redux/chatHistorySlice"
1414
import { useAppDispatch, useAppSelector } from "redux/store"
1515

1616
import styles from "./Settings.module.scss"
@@ -22,7 +22,7 @@ export function Settings() {
2222
const baseURL = useAppSelector(state => state.settings.baseURL)
2323
const model = useAppSelector(state => state.settings.model)
2424
const showStateDiagramStatus = useAppSelector(state => state.settings.showStateDiagramStatus)
25-
const showFullChatHistory = useAppSelector(state => state.chatHistory.showFullChatHistory)
25+
const chatHistoryDisplay = useAppSelector(state => state.chatHistory.chatHistoryDisplay)
2626

2727
const [showSettingsModal, setShowSettingsModal] = useState<boolean>(false)
2828
const closeSettingsModal = () => setShowSettingsModal(false)
@@ -48,10 +48,16 @@ export function Settings() {
4848
return (
4949
<>
5050
<Modal opened={showSettingsModal} onClose={closeSettingsModal} title="Settings">
51-
<Checkbox
52-
checked={showFullChatHistory}
53-
onChange={() => dispatch(toggleShowFullChatHistory())}
54-
label="Show full chat history"
51+
<Select
52+
label="Chat History View"
53+
placeholder="Set chat history complexity"
54+
data={Object.entries(CHAT_HISTORY_DISPLAY_OPTIONS).map(([value,label]) => ({
55+
value,label
56+
}))}
57+
value={chatHistoryDisplay}
58+
onChange={(value) => value && dispatch(setChatHistoryDisplay(
59+
value as ChatHistoryDisplayType
60+
))}
5561
/>
5662
<br/>
5763
<Checkbox

src/redux/chatHistorySlice.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,28 @@ export type LinkQChatMessageType = ChatCompletionMessageParam & {
1313
stage?: StageType,
1414
}
1515

16+
//"simple" is a bare-bones for novice users
17+
//"condensed" shows more info for intermediate users
18+
//"full" shows all the messages for expert users
19+
export const CHAT_HISTORY_DISPLAY_OPTIONS = {
20+
"simple": "Simple View",
21+
"condensed": "Condensed View",
22+
"full": "Full Chat History",
23+
} as const
24+
export type ChatHistoryDisplayType = keyof typeof CHAT_HISTORY_DISPLAY_OPTIONS
25+
1626
const initialState: {
1727
chatIdCounter: number,
1828
fullChatHistory: LinkQChatMessageType[],
1929
simpleChatHistory: LinkQChatMessageType[],
20-
showFullChatHistory: boolean,
30+
chatHistoryDisplay: ChatHistoryDisplayType,
2131
} = {
2232
chatIdCounter: 1,
2333

2434
//state for the full chat history, including system messages and the LLM interfacing with the KG API
2535
fullChatHistory: IS_DEMO_MODE ? DEMO_FULL_HISTORY : [],
2636

27-
//this option toggles showing the full chat history for an ML expert user
28-
//vs hiding the system messages for a non-expert user
29-
showFullChatHistory: true,
37+
chatHistoryDisplay: "condensed",
3038

3139
//state for the filtered chat history (ie no behind-the-scenes system messages)
3240
simpleChatHistory: IS_DEMO_MODE ? DEMO_SIMPLE_HISTORY : [],
@@ -45,8 +53,8 @@ const chatHistorySlice = createSlice({
4553
incrementChatIdCounter: (state) => {
4654
state.chatIdCounter++
4755
},
48-
toggleShowFullChatHistory: state => {
49-
state.showFullChatHistory = !state.showFullChatHistory
56+
setChatHistoryDisplay: (state, action: PayloadAction<ChatHistoryDisplayType>) => {
57+
state.chatHistoryDisplay = action.payload
5058
}
5159
}
5260
})
@@ -57,6 +65,6 @@ export const {
5765
addMessagesToFullChatHistory,
5866
addMessageToSimpleChatHistory,
5967
incrementChatIdCounter,
60-
toggleShowFullChatHistory,
68+
setChatHistoryDisplay,
6169
}
6270
} = chatHistorySlice

0 commit comments

Comments
 (0)