feat: improve prompt, add ability to abort streaming, improve message parser
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user