From 92ff0bbef43b6f34e776e6e1b734bd00ffecb14e Mon Sep 17 00:00:00 2001 From: Jagger <634750802@qq.com> Date: Thu, 19 Sep 2024 10:35:58 +0800 Subject: [PATCH] style(frontend): optimize message view (#293) close #287 close #288 close #289 close #291 --- .github/workflows/release.yml | 5 ++ e2e/prepare-test.sh | 5 ++ e2e/tests/chat.spec.ts | 2 +- e2e/utils/chat.ts | 2 +- .../app/src/components/chat/chat-hooks.tsx | 4 + .../chat/conversation-message-groups.tsx | 2 +- .../app/src/components/chat/conversation.tsx | 2 +- .../src/components/chat/message-answer.tsx | 4 +- .../components/chat/message-beta-alert.tsx | 16 ++++ .../src/components/chat/message-feedback.tsx | 18 +++-- .../app/src/components/chat/message-input.tsx | 6 +- .../components/chat/message-operations.tsx | 81 +++++++++++++++---- frontend/app/src/components/ui/alert.tsx | 2 + frontend/app/src/components/ui/tooltip.tsx | 4 +- frontend/app/src/components/use-size.ts | 6 +- frontend/packages/widget-react/package.json | 1 + frontend/packages/widget-react/src/Widget.tsx | 8 +- frontend/pnpm-lock.yaml | 3 + 18 files changed, 133 insertions(+), 38 deletions(-) create mode 100755 e2e/prepare-test.sh create mode 100644 frontend/app/src/components/chat/message-beta-alert.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3aaa81b..9389cfa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -157,6 +157,11 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps + - name: Fetch Images + run: ./prepare-test.sh + env: + E2E_DOCKER_TAG: sha-${{ github.sha }}-dev + - name: Run tests run: ./start-test.sh env: diff --git a/e2e/prepare-test.sh b/e2e/prepare-test.sh new file mode 100755 index 00000000..af0dfc3d --- /dev/null +++ b/e2e/prepare-test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +docker compose pull frontend background backend tidb redis static-web-server diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index f20312cc..d0da6b13 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -12,7 +12,7 @@ test.describe('Chat', () => { // https://playwright.dev/docs/events#waiting-for-event const chatRequestPromise = getChatRequestPromise(page, baseURL); - const trigger = page.locator('button', { has: page.locator('svg.lucide-arrow-right') }); + const trigger = page.locator('button', { has: page.locator('svg.lucide-arrow-up') }); await trigger.click(); await expect(trigger).toBeDisabled(); diff --git a/e2e/utils/chat.ts b/e2e/utils/chat.ts index 291df694..4fec706b 100644 --- a/e2e/utils/chat.ts +++ b/e2e/utils/chat.ts @@ -20,7 +20,7 @@ export async function testNewChat (page: Page, chatRequest: Request, validatePag expect(chatResponse.ok()).toBe(true); // Feedback button indicates chat ends. - await page.locator('button', { has: page.locator('svg.lucide-message-square-plus') }).waitFor({ state: 'visible' }); + await page.locator('button', { has: page.locator('svg.lucide-thumbs-up') }).waitFor({ state: 'visible' }); return await chatResponse.text(); }); diff --git a/frontend/app/src/components/chat/chat-hooks.tsx b/frontend/app/src/components/chat/chat-hooks.tsx index 4990f398..1f6adc63 100644 --- a/frontend/app/src/components/chat/chat-hooks.tsx +++ b/frontend/app/src/components/chat/chat-hooks.tsx @@ -83,6 +83,7 @@ export function useChats () { export interface ChatMessageGroup { user: ChatMessageController; assistant: ChatMessageController | undefined; + hasFirstAssistantMessage: boolean; } export function useChatController ( @@ -201,6 +202,7 @@ export function useChatMessageGroups (controllers: ChatMessageController[]) { function collectMessageGroups (messageControllers: ChatMessageController[]) { const groups: ChatMessageGroup[] = []; + let hasAssistant: boolean = false; let user: ChatMessageController | undefined; for (let messageController of messageControllers) { @@ -213,7 +215,9 @@ function collectMessageGroups (messageControllers: ChatMessageController[]) { groups.push({ user, assistant: messageController, + hasFirstAssistantMessage: !hasAssistant, }); + hasAssistant = true; } else { console.warn('No matched user message, drop assistant message', messageController.message.id); } diff --git a/frontend/app/src/components/chat/conversation-message-groups.tsx b/frontend/app/src/components/chat/conversation-message-groups.tsx index c531ccf8..d5674a34 100644 --- a/frontend/app/src/components/chat/conversation-message-groups.tsx +++ b/frontend/app/src/components/chat/conversation-message-groups.tsx @@ -106,7 +106,7 @@ function ConversationMessageGroup ({ group }: { group: ChatMessageGroup }) { - + {group.assistant && } diff --git a/frontend/app/src/components/chat/conversation.tsx b/frontend/app/src/components/chat/conversation.tsx index dbbeaaf7..6d456b85 100644 --- a/frontend/app/src/components/chat/conversation.tsx +++ b/frontend/app/src/components/chat/conversation.tsx @@ -71,7 +71,7 @@ export function Conversation ({ open, chat, chatId, history, placeholder, preven
- {size && open &&
+ {size && open && } diff --git a/frontend/app/src/components/chat/message-answer.tsx b/frontend/app/src/components/chat/message-answer.tsx index eef7ee78..b7081d8b 100644 --- a/frontend/app/src/components/chat/message-answer.tsx +++ b/frontend/app/src/components/chat/message-answer.tsx @@ -1,9 +1,10 @@ import { useChatMessageField, useChatMessageStreamContainsState } from '@/components/chat/chat-hooks'; import type { ChatMessageController } from '@/components/chat/chat-message-controller'; import { AppChatStreamState } from '@/components/chat/chat-stream-state'; +import { MessageBetaAlert } from '@/components/chat/message-beta-alert'; import { MessageContent } from '@/components/chat/message-content'; -export function MessageAnswer ({ message }: { message: ChatMessageController | undefined }) { +export function MessageAnswer ({ message, showBetaAlert }: { message: ChatMessageController | undefined, showBetaAlert?: boolean }) { const content = useChatMessageField(message, 'content'); const shouldShow = useChatMessageStreamContainsState(message, AppChatStreamState.GENERATE_ANSWER); @@ -26,6 +27,7 @@ export function MessageAnswer ({ message }: { message: ChatMessageController | u Answer + {showBetaAlert && } ); diff --git a/frontend/app/src/components/chat/message-beta-alert.tsx b/frontend/app/src/components/chat/message-beta-alert.tsx new file mode 100644 index 00000000..dfa757b0 --- /dev/null +++ b/frontend/app/src/components/chat/message-beta-alert.tsx @@ -0,0 +1,16 @@ +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { FlaskConicalIcon } from 'lucide-react'; + +export function MessageBetaAlert () { + return ( + + + + This chatbot is in Beta. + + + All generated information should be verified prior to use. + + + ); +} diff --git a/frontend/app/src/components/chat/message-feedback.tsx b/frontend/app/src/components/chat/message-feedback.tsx index 7b12b675..41a51254 100644 --- a/frontend/app/src/components/chat/message-feedback.tsx +++ b/frontend/app/src/components/chat/message-feedback.tsx @@ -1,20 +1,26 @@ import type { FeedbackParams } from '@/api/chats'; import { usePortalContainer } from '@/components/portal-provider'; import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { Loader2Icon, ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react'; -import { type ReactElement, useEffect, useState } from 'react'; +import { type ReactNode, useEffect, useState } from 'react'; -export function MessageFeedback ({ initial, onFeedback, children }: { initial?: FeedbackParams, onFeedback: (action: 'like' | 'dislike', comment: string) => Promise, children: ReactElement }) { +export function MessageFeedback ({ initial, onFeedback, defaultAction, children }: { initial?: FeedbackParams, defaultAction?: 'like' | 'dislike', onFeedback: (action: 'like' | 'dislike', comment: string) => Promise, children: ReactNode }) { const [open, setOpen] = useState(false); - const [action, setAction] = useState<'like' | 'dislike'>(initial?.feedback_type ?? 'like'); + const [action, setAction] = useState<'like' | 'dislike'>(initial?.feedback_type ?? defaultAction ?? 'like'); // const [detail, setDetail] = useState>(() => (initial ?? {})); const [comment, setComment] = useState(initial?.comment ?? ''); const [running, setRunning] = useState(false); const [deleting, setDeleting] = useState(false); + useEffect(() => { + if (defaultAction && !initial) { + setAction(defaultAction); + } + }, [defaultAction, initial]); + useEffect(() => { if (initial) { setAction(initial.feedback_type); @@ -30,9 +36,7 @@ export function MessageFeedback ({ initial, onFeedback, children }: { initial?: return ( - - {children} - + {children} diff --git a/frontend/app/src/components/chat/message-input.tsx b/frontend/app/src/components/chat/message-input.tsx index a67ad57e..fdbc9ede 100644 --- a/frontend/app/src/components/chat/message-input.tsx +++ b/frontend/app/src/components/chat/message-input.tsx @@ -7,8 +7,8 @@ import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import isHotkey from 'is-hotkey'; -import { ArrowRightIcon } from 'lucide-react'; -import { type ChangeEvent, type Ref, type RefObject, useCallback, useRef, useState } from 'react'; +import { ArrowUpIcon } from 'lucide-react'; +import { type ChangeEvent, type Ref, useCallback, useRef, useState } from 'react'; import TextareaAutosize, { type TextareaAutosizeProps } from 'react-textarea-autosize'; import useSWR from 'swr'; @@ -75,7 +75,7 @@ export function MessageInput ({ } ); diff --git a/frontend/app/src/components/chat/message-operations.tsx b/frontend/app/src/components/chat/message-operations.tsx index 982559ac..6a70749c 100644 --- a/frontend/app/src/components/chat/message-operations.tsx +++ b/frontend/app/src/components/chat/message-operations.tsx @@ -2,11 +2,13 @@ import { useChatMessageField, useChatMessageStreamState, useCurrentChatControlle import { ChatMessageController } from '@/components/chat/chat-message-controller'; import { MessageFeedback } from '@/components/chat/message-feedback'; import { useMessageFeedback } from '@/components/chat/use-message-feedback'; +import { usePortalContainer } from '@/components/portal-provider'; import { Button } from '@/components/ui/button'; -import { TooltipProvider } from '@/components/ui/tooltip'; +import { DialogTrigger } from '@/components/ui/dialog'; +import { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import copy from 'copy-to-clipboard'; -import { ClipboardCheckIcon, ClipboardIcon, MessageSquareHeartIcon, MessageSquarePlusIcon } from 'lucide-react'; +import { CopyCheckIcon, CopyIcon, ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react'; import { useState } from 'react'; export function MessageOperations ({ message }: { message: ChatMessageController }) { @@ -15,6 +17,8 @@ export function MessageOperations ({ message }: { message: ChatMessageController const streamState = useChatMessageStreamState(message); const { feedbackData, feedback: callFeedback, disabled } = useMessageFeedback(message.id, !!traceUrl); const [copied, setCopied] = useState(false); + const [clicked, setClicked] = useState<'like' | 'dislike'>('like'); + const container = usePortalContainer(); if (streamState) { return null; @@ -35,25 +39,68 @@ export function MessageOperations ({ message }: { message: ChatMessageController callFeedback(action, comment)} > - + {feedbackData + ? ( + + ) + : (<> + + + + + + + + + I like this answer :) + + + + + + + + + + + + I dislike this answer :( + + + + )} - + + + + + + + {copied ? 'Copied!' : 'Copy'} + + + ); diff --git a/frontend/app/src/components/ui/alert.tsx b/frontend/app/src/components/ui/alert.tsx index adf2d902..79ba0fec 100644 --- a/frontend/app/src/components/ui/alert.tsx +++ b/frontend/app/src/components/ui/alert.tsx @@ -15,6 +15,8 @@ const alertVariants = cva( "border-yellow-600/50 text-yellow-600 [&>svg]:text-yellow-600 dark:text-yellow-400 dark:border-yellow-400 dark:[&>svg]:text-yellow-400", success: "border-green-600/50 text-green-600 [&>svg]:text-green-600 dark:text-green-400 dark:border-green-400 dark:[&>svg]:text-green-400", + info: + "border-sky-600/50 text-sky-600 [&>svg]:text-sky-600 dark:text-sky-400 dark:border-sky-400 dark:[&>svg]:text-sky-400", }, }, defaultVariants: { diff --git a/frontend/app/src/components/ui/tooltip.tsx b/frontend/app/src/components/ui/tooltip.tsx index 30fc44d9..8c1de8ff 100644 --- a/frontend/app/src/components/ui/tooltip.tsx +++ b/frontend/app/src/components/ui/tooltip.tsx @@ -11,6 +11,8 @@ const Tooltip = TooltipPrimitive.Root const TooltipTrigger = TooltipPrimitive.Trigger +const TooltipPortal = TooltipPrimitive.Portal + const TooltipContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -27,4 +29,4 @@ const TooltipContent = React.forwardRef< )) TooltipContent.displayName = TooltipPrimitive.Content.displayName -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } +export { Tooltip, TooltipTrigger, TooltipPortal, TooltipContent, TooltipProvider } diff --git a/frontend/app/src/components/use-size.ts b/frontend/app/src/components/use-size.ts index 2d936108..78dc74f1 100644 --- a/frontend/app/src/components/use-size.ts +++ b/frontend/app/src/components/use-size.ts @@ -1,9 +1,9 @@ -import { useEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; export function useSize () { const [size, setSize] = useState(undefined); const ref = useRef(null); - useEffect(() => { + useLayoutEffect(() => { const el = ref.current; if (el) { const ro = new ResizeObserver(() => { @@ -21,5 +21,5 @@ export function useSize () { return { ref, size, - } + }; } \ No newline at end of file diff --git a/frontend/packages/widget-react/package.json b/frontend/packages/widget-react/package.json index 3afc5ac5..2b140515 100644 --- a/frontend/packages/widget-react/package.json +++ b/frontend/packages/widget-react/package.json @@ -25,6 +25,7 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "lucide-react": "^0.400.0", "postcss": "^8", "rollup-plugin-visualizer": "^5.12.0", "sass": "^1.77.6", diff --git a/frontend/packages/widget-react/src/Widget.tsx b/frontend/packages/widget-react/src/Widget.tsx index 19dff61d..e8c2d9fb 100644 --- a/frontend/packages/widget-react/src/Widget.tsx +++ b/frontend/packages/widget-react/src/Widget.tsx @@ -7,11 +7,12 @@ import { useGtagFn } from '@/components/gtag-provider'; import { PortalProvider } from '@/components/portal-provider'; import { BootstrapStatusProvider } from '@/components/system/BootstrapStatusProvider'; import { Button } from '@/components/ui/button'; -import { Dialog, DialogDescription, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Dialog, DialogClose, DialogDescription, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { type ExperimentalFeatures, ExperimentalFeaturesProvider } from '@/experimental/experimental-features-provider'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { XIcon } from 'lucide-react' import './Widget.css'; export interface WidgetProps { @@ -147,7 +148,10 @@ export const Widget = forwardRef(({ container, trig - + + + + logo diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index baa1f665..f1ca6a8c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -422,6 +422,9 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.7 version: 0.4.7(eslint@8.57.0) + lucide-react: + specifier: ^0.400.0 + version: 0.400.0(react@18.3.1) postcss: specifier: ^8 version: 8.4.39