Use Turnstyle UX pattern to group Khoj chats into conversation turns

A turn is defined as a single back and forth between the user and AI.

A typical user - agent interaction is more strongly coupled into a
request, response pair than a typical chat message interaction between
humans.

Turnstyle UX emphasis this natural coupling of the request, response
pairs by grouping a conversation turn more strongly together than
the older, more typical chat bubble UX.

Turnstyle UX renders each user's query and AI's response into a
section heading, section content respectively for cleaner, more
natural demarcation of a single interaction
This commit is contained in:
Debanjum Singh Solanky 2024-07-31 22:27:15 +05:30
parent 5a8ea884a9
commit dc47ba065c
3 changed files with 78 additions and 63 deletions

View file

@ -265,7 +265,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
chatMessage={chatMessage}
customClassName='fullHistory'
borderLeftColor={`${data?.agent.color}-500`}
isLastMessage={index === data.chat.length - 1}
isLastMessage={index === data.chat.length - 2}
/>
))}
{
@ -288,6 +288,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
}
customClassName='fullHistory'
borderLeftColor={`${data?.agent.color}-500`}
isLastMessage={true}
/>
{
message.trainOfThought &&
@ -313,7 +314,6 @@ export default function ChatHistory(props: ChatHistoryProps) {
}
customClassName='fullHistory'
borderLeftColor={`${data?.agent.color}-500`}
isLastMessage={true}
/>
</React.Fragment>
)

View file

@ -1,14 +1,22 @@
div.chatMessageContainer {
display: flex;
flex-direction: column;
margin: 12px;
border-radius: 16px;
}
div.chatMessageContainer.khoj {
margin: 0;
padding: 8px 16px 0 16px;
box-shadow: 0 4px 10px var(--box-shadow-color);
border-top: transparent;
}
div.chatMessageContainer.you {
margin-top: 12px;
padding-top: 16px;
box-shadow: none;
border-top: transparent;
}
div.chatMessageWrapper {
padding-left: 1rem;
padding-bottom: 1rem;
max-width: 80vw;
}
div.chatMessageWrapper p:not(:last-child) {
@ -25,23 +33,22 @@ div.youfullHistory {
}
div.chatMessageContainer.youfullHistory {
padding-left: 0px;
padding-left: 16px;
}
div.you {
background-color: hsla(var(--secondary));
align-self: flex-end;
border-radius: 16px;
background-color: transparent;
align-self: flex-start;
}
div.khoj {
background-color: transparent;
color: hsl(var(--accent-foreground));
align-self: flex-start;
padding-left: 1rem;
}
div.khojChatMessage {
padding-top: 8px;
padding-left: 16px;
}
@ -64,8 +71,6 @@ div.author {
}
div.chatFooter {
display: flex;
justify-content: space-between;
min-height: 28px;
}

View file

@ -10,7 +10,7 @@ import 'katex/dist/katex.min.css';
import { TeaserReferencesSection, constructAllReferences } from '../referencePanel/referencePanel';
import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, SpeakerHigh, MagnifyingGlass, Pause, Palette } from '@phosphor-icons/react';
import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, SpeakerHigh, MagnifyingGlass, Pause, Palette, Clock } from '@phosphor-icons/react';
import DOMPurify from 'dompurify';
import { InlineLoading } from '../loading/loading';
@ -331,8 +331,12 @@ export default function ChatMessage(props: ChatMessageProps) {
}
function chatMessageWrapperClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageWrapper];
classes.push(styles[chatMessage.by]);
let classes = [styles.chatMessageWrapper, styles[chatMessage.by]];
if (chatMessage.by === "you") {
let fontClass = chatMessage.message.length > 100 ? "text-md" : "text-xl";
classes.push(fontClass);
}
if (chatMessage.by === "khoj") {
classes.push(`border-l-4 border-opacity-50 ${"border-l-" + props.borderLeftColor || "border-l-orange-400"}`);
}
@ -429,58 +433,64 @@ export default function ChatMessage(props: ChatMessageProps) {
notesReferenceCardData={allReferences.notesReferenceCardData}
onlineReferenceCardData={allReferences.onlineReferenceCardData} />
</div>
<div className={styles.chatFooter}>
{
(isHovering || props.isMobileWidth || props.isLastMessage || isPlaying) &&
props.chatMessage.by === "you" &&
(
<>
<div title={formatDate(props.chatMessage.created)} className={`text-gray-400 relative top-0 left-4`}>
{renderTimeStamp(props.chatMessage.created)}
</div>
<div className={`${styles.chatButtons} shadow-sm`}>
{
(props.chatMessage.by === "khoj") &&
(
isPlaying ?
(
interrupted ?
<InlineLoading iconClassName='p-0' className='m-0' />
: <button title="Pause Speech" onClick={(event) => setInterrupted(true)}>
<Pause alt="Pause Message" color='hsl(var(--muted-foreground))' />
</button>
)
: <button title="Speak" onClick={(event) => playTextToSpeech()}>
<SpeakerHigh alt="Speak Message" color='hsl(var(--muted-foreground))' />
</button>
)
}
<button title="Copy" className={`${styles.copyButton}`} onClick={() => {
navigator.clipboard.writeText(props.chatMessage.message);
setCopySuccess(true);
}}>
{
copySuccess ?
<Copy alt="Copied Message" weight="fill" color='green' />
: <Copy alt="Copy Message" color='hsl(var(--muted-foreground))' />
}
</button>
{
(props.chatMessage.by === "khoj") &&
(
props.chatMessage.intent ?
<FeedbackButtons
uquery={props.chatMessage.intent.query}
kquery={props.chatMessage.message} />
: <FeedbackButtons
uquery={props.chatMessage.rawQuery || props.chatMessage.message}
kquery={props.chatMessage.message} />
)
}
</div>
</>
<div className={`${styles.chatFooter} flex justify-start`}>
{
(isHovering || props.isMobileWidth || props.isLastMessage || isPlaying) && (
<div title={formatDate(props.chatMessage.created)} className={`text-gray-400 relative top-0 text-sm`}>
<Clock className={`inline mr-2 top-1 text-gray-400`} />
{renderTimeStamp(props.chatMessage.created)}
</div>
)
}
</div>
) || props.chatMessage.by === "khoj" &&
(
<div className={`${styles.chatFooter} flex justify-end`}>
{
(isHovering || props.isMobileWidth || props.isLastMessage || isPlaying) &&
(
<div className={`${styles.chatButtons} shadow-sm`}>
{
isPlaying ?
(
interrupted ?
<InlineLoading iconClassName='p-0' className='m-0' />
: <button title="Pause Speech" onClick={(event) => setInterrupted(true)}>
<Pause alt="Pause Message" color='hsl(var(--muted-foreground))' />
</button>
)
: <button title="Speak" onClick={(event) => playTextToSpeech()}>
<SpeakerHigh alt="Speak Message" color='hsl(var(--muted-foreground))' />
</button>
}
<button title="Copy" className={`${styles.copyButton}`} onClick={() => {
navigator.clipboard.writeText(props.chatMessage.message);
setCopySuccess(true);
}}>
{
copySuccess ?
<Copy alt="Copied Message" weight="fill" color='green' />
: <Copy alt="Copy Message" color='hsl(var(--muted-foreground))' />
}
</button>
{
props.chatMessage.intent ?
<FeedbackButtons
uquery={props.chatMessage.intent.query}
kquery={props.chatMessage.message} />
: <FeedbackButtons
uquery={props.chatMessage.rawQuery || props.chatMessage.message}
kquery={props.chatMessage.message} />
}
</div>
)
}
</div>
)
}
</div>
</div>
)
}