Skip to content

Commit

Permalink
style(frontend): refine stream state history
Browse files Browse the repository at this point in the history
  • Loading branch information
634750802 committed Aug 21, 2024
1 parent dcfc037 commit 2a5964c
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DebugInfo } from '@/components/chat/debug-info';
import { MessageAnnotationHistory } from '@/components/chat/message-annotation-history';
import { MessageAnswer } from '@/components/chat/message-answer';
import { MessageContextSources } from '@/components/chat/message-content-sources';
import { MessageError } from '@/components/chat/message-error';
import { MessageOperations } from '@/components/chat/message-operations';
import { MessageSection } from '@/components/chat/message-section';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -106,6 +107,8 @@ function ConversationMessageGroup ({ group }: { group: ChatMessageGroup }) {
<MessageAnswer message={group.assistant} />
</MessageSection>

{group.assistant && <MessageError message={group.assistant} />}

{group.assistant && <MessageOperations message={group.assistant} />}
</section>
);
Expand Down
141 changes: 77 additions & 64 deletions frontend/app/src/components/chat/message-annotation-history.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useChatMessageStreamHistoryStates, useChatMessageStreamState } from '@/components/chat/chat-hooks';
import type { ChatMessageController } from '@/components/chat/chat-message-controller';
import { useChatMessageField, useChatMessageStreamHistoryStates, useChatMessageStreamState } from '@/components/chat/chat-hooks';
import type { ChatMessageController, OngoingState, OngoingStateHistoryItem } from '@/components/chat/chat-message-controller';
import { isNotFinished } from '@/components/chat/utils';
import { DiffSeconds } from '@/components/diff-seconds';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
import { motion, type Target } from 'framer-motion';
import { CheckCircleIcon, ChevronUpIcon, ClockIcon, Loader2Icon } from 'lucide-react';
import { InformationCircleIcon } from 'nextra/icons';
import { useEffect, useState } from 'react';

const CheckedCircle = motion(CheckCircleIcon);

export function MessageAnnotationHistory ({ message }: { message: ChatMessageController | undefined }) {
const [show, setShow] = useState(true);
const history = useChatMessageStreamHistoryStates(message);
const current = useChatMessageStreamState(message);
const error = useChatMessageField(message, 'error');

const finished = !isNotFinished(current);
const finished = !isNotFinished(current) || !!error;

useEffect(() => {
if (finished) {
Expand Down Expand Up @@ -42,62 +41,11 @@ export function MessageAnnotationHistory ({ message }: { message: ChatMessageCon
<ol
className="text-sm mt-4"
>
{history?.map(({ state, time }, index, history) => (
index > 0 && (
<motion.li
className={cn('relative mb-2', index === history.length - 1 && current && !current.finished && 'mb-2')}
key={index}
initial={{
opacity: 0.5,
}}
animate={{
opacity: 1,
}}
>
{index > 1 && <span className="absolute left-2 bg-green-500 h-2" style={{ width: 1, top: -8 }} />}
<div className="flex gap-2 items-center">
<CheckedCircle
className="size-4 text-green-500"
initial={{
color: 'rgb(113 113 122)',
}}
animate={{
color: 'rgb(34 197 94)',
}}
/>
<span>
{state.display}
</span>
{index > 0 && <DiffSeconds className="text-muted-foreground text-xs" from={history[index - 1].time} to={time} />}
</div>
{state.message && <div className="ml-2 pl-4 text-muted-foreground text-xs border-l border-l-green-500 pt-1">{state.message}</div>}
</motion.li>
)
{history?.map((item, index, history) => (
index > 0 && <MessageAnnotationHistoryItem key={index} index={index} history={history} item={item} />
))}
{current && !current.finished && <motion.li
key={current.state}
className="relative space-y-1"
initial={{
opacity: 0,
height: 0,
x: -40,
}}
animate={{
opacity: 0.5,
height: 'auto',
x: 0,
}}
>
<div className="flex gap-2 items-center">
{(history?.length ?? 0) > 1 && <span className="absolute left-2 opacity-50 bg-zinc-500 h-2" style={{ width: 1, top: -8 }} />}
<Loader2Icon className="size-4 animate-spin repeat-infinite text-muted-foreground" />
<span>
{current.display}
</span>
{history && history.length > 0 && <DiffSeconds className="text-muted-foreground text-xs" from={history[history.length - 1].time} />}
</div>
{current.message && <div className="ml-2 pl-4 text-muted-foreground text-xs border-l border-l-green-500 pt-1">{current.message}</div>}
</motion.li>}
{error && <MessageAnnotationHistoryError history={history} error={error} />}
{current && !current.finished && <MessageAnnotationCurrent history={history} current={current} />}
</ol>
<button className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors" onClick={() => setShow(false)}>
<ChevronUpIcon className="size-4 mr-1" />
Expand All @@ -110,8 +58,73 @@ export function MessageAnnotationHistory ({ message }: { message: ChatMessageCon
animate={show ? { height: 0, opacity: 0, overflow: 'visible', pointerEvents: 'none', scale: 0.5 } : { height: 'auto', opacity: 1, scale: 1, pointerEvents: 'auto' }}
>
<ClockIcon className="size-3" />
<DiffSeconds from={message?.message?.created_at} to={message?.message?.finished_at} />
{error ? 'Not finished' : <DiffSeconds from={message?.message?.created_at} to={message?.message?.finished_at} />}
</motion.button>
</div>
);
}
}

const CheckedCircle = motion(CheckCircleIcon);
const InformationCircle = motion(InformationCircleIcon);

const itemInitial: Target = { opacity: 0.5 };
const itemAnimate: Target = { opacity: 1 };

const itemIconInitial: Target = { color: 'rgb(113 113 122 / 50)' };
const itemSuccessIconAnimate: Target = { color: 'rgb(34 197 94)' };
const itemErrorIconAnimate: Target = { color: 'rgb(239 68 68)' };

function MessageAnnotationHistoryItem ({ history, item: { state, time }, index }: { history: OngoingStateHistoryItem[], index: number, item: OngoingStateHistoryItem }) {
return (
<motion.li className="relative mb-2" initial={itemInitial} animate={itemAnimate}>
{index > 1 && <span className="absolute left-2 bg-green-500/50 h-2" style={{ width: 1, top: -8 }} />}
<div className="flex gap-2 items-center">
<CheckedCircle className="size-4" initial={itemIconInitial} animate={itemSuccessIconAnimate} />
<span>{state.display}</span>
{index > 0 && <DiffSeconds className="text-muted-foreground text-xs" from={history[index - 1].time} to={time} />}
</div>
{state.message && <div className="ml-2 pl-4 text-muted-foreground text-xs border-l border-l-green-500/50 pt-1">{state.message}</div>}
</motion.li>
);
}

function MessageAnnotationHistoryError ({ history, error }: { history: OngoingStateHistoryItem[], error: string }) {
return (
<motion.li className="relative mb-2" initial={itemInitial} animate={itemAnimate}>
{history.length > 0 && <span className="absolute left-2 bg-muted-foreground h-2" style={{ width: 1, top: -8 }} />}
<div className="flex gap-2 items-center">
<InformationCircle className="size-4" initial={itemIconInitial} animate={itemErrorIconAnimate} />
<span>{error}</span>
</div>
</motion.li>
);
}

function MessageAnnotationCurrent ({ history, current }: { history: OngoingStateHistoryItem[], current: OngoingState }) {
return (
<motion.li
key={current.state}
className="relative space-y-1"
initial={{
opacity: 0,
height: 0,
x: -40,
}}
animate={{
opacity: 0.5,
height: 'auto',
x: 0,
}}
>
<div className="flex gap-2 items-center">
{(history?.length ?? 0) > 1 && <span className="absolute left-2 h-2 bg-zinc-500/50" style={{ width: 1, top: -8 }} />}
<Loader2Icon className="size-4 animate-spin repeat-infinite text-muted-foreground" />
<span>
{current.display}
</span>
{history && history.length > 0 && <DiffSeconds className="text-muted-foreground text-xs" from={history[history.length - 1].time} />}
</div>
{current.message && <div className="ml-2 pl-4 text-muted-foreground text-xs border-l border-l-zinc-500 pt-1">{current.message}</div>}
</motion.li>
);
}
2 changes: 0 additions & 2 deletions frontend/app/src/components/chat/message-answer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useChatMessageField, useChatMessageStreamContainsState } from '@/compon
import type { ChatMessageController } from '@/components/chat/chat-message-controller';
import { AppChatStreamState } from '@/components/chat/chat-stream-state';
import { MessageContent } from '@/components/chat/message-content';
import { MessageError } from '@/components/chat/message-error';

export function MessageAnswer ({ message }: { message: ChatMessageController | undefined }) {
const content = useChatMessageField(message, 'content');
Expand All @@ -19,7 +18,6 @@ export function MessageAnswer ({ message }: { message: ChatMessageController | u
<img className="hidden dark:block h-4" src="/answer-white.svg" alt="logo" />
Answer
</div>
{message && <MessageError message={message} />}
<MessageContent message={message} />
</>
);
Expand Down

0 comments on commit 2a5964c

Please sign in to comment.