feat: improve prompt, add ability to abort streaming, improve message parser

This commit is contained in:
Dominic Elm
2024-07-18 11:10:12 +02:00
parent 637ad2b870
commit 012b5bae80
12 changed files with 633 additions and 160 deletions

View File

@@ -1,8 +1,9 @@
import { useStore } from '@nanostores/react';
import { AnimatePresence, motion } from 'framer-motion';
import { computed } from 'nanostores';
import { useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki';
import { chatStore } from '../../lib/stores/chat';
import { getArtifactKey, workbenchStore, type ActionState } from '../../lib/stores/workbench';
import { classNames } from '../../utils/classNames';
import { cubicEasingFn } from '../../utils/easings';
@@ -25,9 +26,11 @@ interface ArtifactProps {
messageId: string;
}
export function Artifact({ artifactId, messageId }: ArtifactProps) {
export const Artifact = memo(({ artifactId, messageId }: ArtifactProps) => {
const userToggledActions = useRef(false);
const [showActions, setShowActions] = useState(false);
const chat = useStore(chatStore);
const artifacts = useStore(workbenchStore.artifacts);
const artifact = artifacts[getArtifactKey(artifactId, messageId)];
@@ -37,6 +40,17 @@ export function Artifact({ artifactId, messageId }: ArtifactProps) {
}),
);
const toggleActions = () => {
userToggledActions.current = true;
setShowActions(!showActions);
};
useEffect(() => {
if (actions.length && !showActions && !userToggledActions.current) {
setShowActions(true);
}
}, [actions]);
return (
<div className="flex flex-col overflow-hidden border rounded-lg w-full">
<div className="flex">
@@ -48,7 +62,7 @@ export function Artifact({ artifactId, messageId }: ArtifactProps) {
}}
>
<div className="flex items-center px-6 bg-gray-100/50">
{!artifact?.closed ? (
{!artifact?.closed && !chat.aborted ? (
<div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
) : (
<div className="i-ph:code-bold scale-130 text-gray-600"></div>
@@ -67,7 +81,7 @@ export function Artifact({ artifactId, messageId }: ArtifactProps) {
exit={{ width: 0 }}
transition={{ duration: 0.15, ease: cubicEasingFn }}
className="hover:bg-gray-200"
onClick={() => setShowActions(!showActions)}
onClick={toggleActions}
>
<div className="p-4">
<div className={showActions ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
@@ -98,9 +112,9 @@ export function Artifact({ artifactId, messageId }: ArtifactProps) {
const { status, type, content, abort } = action;
return (
<li key={index} className={classNames(getTextColor(action.status))}>
<li key={index}>
<div className="flex items-center gap-1.5">
<div className="text-lg">
<div className={classNames('text-lg', getTextColor(action.status))}>
{status === 'running' ? (
<div className="i-svg-spinners:90-ring-with-bg"></div>
) : status === 'pending' ? (
@@ -136,7 +150,7 @@ export function Artifact({ artifactId, messageId }: ArtifactProps) {
</AnimatePresence>
</div>
);
}
});
function getTextColor(status: ActionState['status']) {
switch (status) {

View File

@@ -16,6 +16,7 @@ interface BaseChatProps {
enhancingPrompt?: boolean;
promptEnhanced?: boolean;
input?: string;
handleStop?: () => void;
sendMessage?: () => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
enhancePrompt?: () => void;
@@ -38,6 +39,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
sendMessage,
handleInputChange,
enhancePrompt,
handleStop,
},
ref,
) => {
@@ -111,7 +113,22 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
placeholder="How can Bolt help you today?"
translate="no"
/>
<ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
<ClientOnly>
{() => (
<SendButton
show={input.length > 0 || isStreaming}
isStreaming={isStreaming}
onClick={() => {
if (isStreaming) {
handleStop?.();
return;
}
sendMessage?.();
}}
/>
)}
</ClientOnly>
<div className="flex justify-between text-sm p-4 pt-2">
<div className="flex gap-1 items-center">
<IconButton icon="i-ph:microphone-duotone" className="-ml-1" />

View File

@@ -2,6 +2,8 @@ import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { useEffect, useRef, useState } from 'react';
import { useMessageParser, usePromptEnhancer } from '../../lib/hooks';
import { chatStore } from '../../lib/stores/chat';
import { workbenchStore } from '../../lib/stores/workbench';
import { cubicEasingFn } from '../../utils/easings';
import { createScopedLogger } from '../../utils/logger';
import { BaseChat } from './BaseChat';
@@ -15,7 +17,7 @@ export function Chat() {
const [animationScope, animate] = useAnimate();
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit } = useChat({
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop } = useChat({
api: '/api/chat',
onError: (error) => {
logger.error(error);
@@ -42,6 +44,12 @@ export function Chat() {
}
};
const abort = () => {
stop();
chatStore.setKey('aborted', true);
workbenchStore.abortAllActions();
};
useEffect(() => {
const textarea = textareaRef.current;
@@ -70,6 +78,8 @@ export function Chat() {
return;
}
chatStore.setKey('aborted', false);
runAnimation();
handleSubmit();
resetEnhancer();
@@ -88,6 +98,7 @@ export function Chat() {
promptEnhanced={promptEnhanced}
sendMessage={sendMessage}
handleInputChange={handleInputChange}
handleStop={abort}
messages={messages.map((message, i) => {
if (message.role === 'user') {
return message;

View File

@@ -2,12 +2,13 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
interface SendButtonProps {
show: boolean;
isStreaming?: boolean;
onClick?: VoidFunction;
}
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
export function SendButton({ show, onClick }: SendButtonProps) {
export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
return (
<AnimatePresence>
{show ? (
@@ -22,7 +23,9 @@ export function SendButton({ show, onClick }: SendButtonProps) {
onClick?.();
}}
>
<div className="i-ph:arrow-right text-xl"></div>
<div className="text-lg">
{!isStreaming ? <div className="i-ph:arrow-right"></div> : <div className="i-ph:stop-circle-bold"></div>}
</div>
</motion.button>
) : null}
</AnimatePresence>