mirror of
https://github.com/langgenius/webapp-conversation.git
synced 2025-12-08 09:12:29 +08:00
Merge pull request #182 from lyzno1/feature/streamdown-and-fixes
feat: Streamdown integration and UI/UX improvements
This commit is contained in:
@ -3,14 +3,14 @@ import { NextResponse } from 'next/server'
|
||||
import { client, getInfo } from '@/app/api/utils/common'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: {
|
||||
params: { conversationId: string }
|
||||
params: Promise<{ conversationId: string }>
|
||||
}) {
|
||||
const body = await request.json()
|
||||
const {
|
||||
auto_generate,
|
||||
name,
|
||||
} = body
|
||||
const { conversationId } = params
|
||||
const { conversationId } = await params
|
||||
const { user } = getInfo(request)
|
||||
|
||||
// auto generate name
|
||||
|
||||
@ -3,13 +3,13 @@ import { NextResponse } from 'next/server'
|
||||
import { client, getInfo } from '@/app/api/utils/common'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: {
|
||||
params: { messageId: string }
|
||||
params: Promise<{ messageId: string }>
|
||||
}) {
|
||||
const body = await request.json()
|
||||
const {
|
||||
rating,
|
||||
} = body
|
||||
const { messageId } = params
|
||||
const { messageId } = await params
|
||||
const { user } = getInfo(request)
|
||||
const { data } = await client.messageFeedback(messageId, rating, user)
|
||||
return NextResponse.json(data)
|
||||
|
||||
@ -15,10 +15,10 @@ export const getInfo = (request: NextRequest) => {
|
||||
}
|
||||
|
||||
export const setSession = (sessionId: string) => {
|
||||
if (APP_INFO.disable_session_same_site)
|
||||
return { 'Set-Cookie': `session_id=${sessionId}; SameSite=None; Secure` }
|
||||
if (APP_INFO.disable_session_same_site)
|
||||
{ return { 'Set-Cookie': `session_id=${sessionId}; SameSite=None; Secure` } }
|
||||
|
||||
return { 'Set-Cookie': `session_id=${sessionId}` }
|
||||
return { 'Set-Cookie': `session_id=${sessionId}` }
|
||||
}
|
||||
|
||||
export const client = new ChatClient(API_KEY, API_URL || undefined)
|
||||
|
||||
@ -32,12 +32,15 @@ const ImageGallery: FC<Props> = ({
|
||||
}) => {
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
const imgNum = srcs.length
|
||||
const validSrcs = srcs.filter(src => src && src.trim() !== '')
|
||||
const imgNum = validSrcs.length
|
||||
const imgStyle = getWidthStyle(imgNum)
|
||||
|
||||
if (imgNum === 0) { return null }
|
||||
|
||||
return (
|
||||
<div className={cn(s[`img-${imgNum}`], 'flex flex-wrap')}>
|
||||
{/* TODO: support preview */}
|
||||
{srcs.map((src, index) => (
|
||||
{validSrcs.map((src, index) => (
|
||||
<img
|
||||
key={index}
|
||||
className={s.item}
|
||||
|
||||
@ -9,7 +9,7 @@ interface StreamdownMarkdownProps {
|
||||
|
||||
export function StreamdownMarkdown({ content, className = '' }: StreamdownMarkdownProps) {
|
||||
return (
|
||||
<div className={`markdown-body streamdown-markdown ${className}`}>
|
||||
<div className={`streamdown-markdown ${className}`}>
|
||||
<Streamdown>{content}</Streamdown>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import classNames from 'classnames'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip' // fixed version to 5.8.3 https://github.com/ReactTooltip/react-tooltip/issues/972
|
||||
import 'react-tooltip/dist/react-tooltip.css'
|
||||
import React, { useState } from 'react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
interface TooltipProps {
|
||||
selector: string
|
||||
@ -15,6 +14,10 @@ interface TooltipProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const arrow = (
|
||||
<svg className="absolute text-white h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255"><polygon className="fill-current" points="0,0 127.5,127.5 255,0"></polygon></svg>
|
||||
)
|
||||
|
||||
const Tooltip: FC<TooltipProps> = ({
|
||||
selector,
|
||||
content,
|
||||
@ -24,22 +27,31 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
className,
|
||||
clickable,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const triggerMethod = clickable ? 'click' : 'hover'
|
||||
|
||||
return (
|
||||
<div className='tooltip-container'>
|
||||
{React.cloneElement(children as React.ReactElement, {
|
||||
'data-tooltip-id': selector,
|
||||
})
|
||||
}
|
||||
<ReactTooltip
|
||||
id={selector}
|
||||
content={content}
|
||||
className={classNames('!bg-white !text-xs !font-normal !text-gray-700 !shadow-lg !opacity-100', className)}
|
||||
place={position}
|
||||
clickable={clickable}
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement={position}
|
||||
offset={10}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
data-selector={selector}
|
||||
onClick={() => triggerMethod === 'click' && setOpen(v => !v)}
|
||||
onMouseEnter={() => triggerMethod === 'hover' && setOpen(true)}
|
||||
onMouseLeave={() => triggerMethod === 'hover' && setOpen(false)}
|
||||
>
|
||||
{htmlContent && htmlContent}
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
{children}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[999]">
|
||||
<div className={classNames('relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg', className)}>
|
||||
{htmlContent ?? content}
|
||||
{arrow}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -187,7 +187,7 @@ const Answer: FC<IAnswerProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${s.answerWrap}`}>
|
||||
<div className={`${s.answerWrap} max-w-[calc(100%-3rem)]`}>
|
||||
<div className={`${s.answer} relative text-sm text-gray-900`}>
|
||||
<div className={`ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl ${workflowProcess && 'min-w-[480px]'}`}>
|
||||
{workflowProcess && (
|
||||
|
||||
@ -170,7 +170,7 @@ const Chat: FC<IChatProps> = ({
|
||||
</div>
|
||||
{
|
||||
!isHideSendInput && (
|
||||
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
|
||||
<div className='fixed z-10 bottom-0 left-1/2 transform -translate-x-1/2 pc:ml-[122px] tablet:ml-[96px] mobile:ml-0 pc:w-[794px] tablet:w-[794px] max-w-full mobile:w-full px-3.5'>
|
||||
<div className='p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto'>
|
||||
{
|
||||
visionConfig?.enabled && (
|
||||
@ -217,8 +217,8 @@ const Chat: FC<IChatProps> = ({
|
||||
onKeyDown={handleKeyDown}
|
||||
autoSize
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 flex items-center h-8">
|
||||
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
|
||||
<div className="absolute bottom-2 right-6 flex items-center h-8">
|
||||
<div className={`${s.count} mr-3 h-5 leading-5 text-sm bg-gray-50 text-gray-500 px-2 rounded`}>{query.trim().length}</div>
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
|
||||
@ -4,7 +4,7 @@ import React from 'react'
|
||||
import type { IChatItem } from '../type'
|
||||
import s from '../style.module.css'
|
||||
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import StreamdownMarkdown from '@/app/components/base/streamdown-markdown'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
|
||||
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'useCurrentUserAvatar'> & {
|
||||
@ -23,7 +23,7 @@ const Question: FC<IQuestionProps> = ({ id, content, useCurrentUserAvatar, imgSr
|
||||
{imgSrcs && imgSrcs.length > 0 && (
|
||||
<ImageGallery srcs={imgSrcs} />
|
||||
)}
|
||||
<Markdown content={content} />
|
||||
<StreamdownMarkdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -174,8 +174,15 @@ const Main: FC<IMainProps> = () => {
|
||||
const [chatList, setChatList, getChatList] = useGetState<ChatItem[]>([])
|
||||
const chatListDomRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
// scroll to bottom
|
||||
if (chatListDomRef.current) { chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight }
|
||||
// scroll to bottom with page-level scrolling
|
||||
if (chatListDomRef.current) {
|
||||
setTimeout(() => {
|
||||
chatListDomRef.current?.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'end',
|
||||
})
|
||||
}, 50)
|
||||
}
|
||||
}, [chatList, currConversationId])
|
||||
// user can not edit inputs if user had send message
|
||||
const canEditInputs = !chatList.some(item => item.isAnswer === false) && isNewConversation
|
||||
@ -677,18 +684,16 @@ const Main: FC<IMainProps> = () => {
|
||||
|
||||
{
|
||||
hasSetInputs && (
|
||||
<div className='relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full pb-[66px] mx-auto mb-3.5 overflow-hidden'>
|
||||
<div className='h-full overflow-y-auto' ref={chatListDomRef}>
|
||||
<Chat
|
||||
chatList={chatList}
|
||||
onSend={handleSend}
|
||||
onFeedback={handleFeedback}
|
||||
isResponding={isResponding}
|
||||
checkCanSend={checkCanSend}
|
||||
visionConfig={visionConfig}
|
||||
fileConfig={fileConfig}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative grow pc:w-[794px] max-w-full mobile:w-full pb-[180px] mx-auto mb-3.5' ref={chatListDomRef}>
|
||||
<Chat
|
||||
chatList={chatList}
|
||||
onSend={handleSend}
|
||||
onFeedback={handleFeedback}
|
||||
isResponding={isResponding}
|
||||
checkCanSend={checkCanSend}
|
||||
visionConfig={visionConfig}
|
||||
fileConfig={fileConfig}
|
||||
/>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -37,7 +37,6 @@ const Welcome: FC<IWelcomeProps> = ({
|
||||
savedInputs,
|
||||
onInputsChange,
|
||||
}) => {
|
||||
console.log(promptConfig)
|
||||
const { t } = useTranslation()
|
||||
const hasVar = promptConfig.prompt_variables.length > 0
|
||||
const [isFold, setIsFold] = useState<boolean>(true)
|
||||
|
||||
@ -3,12 +3,12 @@ import { getLocaleOnServer } from '@/i18n/server'
|
||||
import './styles/globals.css'
|
||||
import './styles/markdown.scss'
|
||||
|
||||
const LocaleLayout = ({
|
||||
const LocaleLayout = async ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const locale = getLocaleOnServer()
|
||||
const locale = await getLocaleOnServer()
|
||||
return (
|
||||
<html lang={locale ?? 'en'} className="h-full">
|
||||
<body className="h-full">
|
||||
|
||||
@ -8,7 +8,7 @@ export const APP_INFO: AppInfo = {
|
||||
copyright: '',
|
||||
privacy_policy: '',
|
||||
default_language: 'en',
|
||||
disable_session_same_site: false, // set it to true if you want to embed the chatbot in an iframe
|
||||
disable_session_same_site: false, // set it to true if you want to embed the chatbot in an iframe
|
||||
}
|
||||
|
||||
export const isShowPrompt = false
|
||||
|
||||
@ -34,4 +34,3 @@ const translation = {
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
|
||||
@ -31,4 +31,3 @@ const translation = {
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
|
||||
@ -101,4 +101,3 @@ const translation = {
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
|
||||
@ -6,19 +6,20 @@ import { match } from '@formatjs/intl-localematcher'
|
||||
import type { Locale } from '.'
|
||||
import { i18n } from '.'
|
||||
|
||||
export const getLocaleOnServer = (): Locale => {
|
||||
export const getLocaleOnServer = async (): Promise<Locale> => {
|
||||
// @ts-expect-error locales are readonly
|
||||
const locales: string[] = i18n.locales
|
||||
|
||||
let languages: string[] | undefined
|
||||
// get locale from cookie
|
||||
const localeCookie = cookies().get('locale')
|
||||
const localeCookie = (await cookies()).get('locale')
|
||||
languages = localeCookie?.value ? [localeCookie.value] : []
|
||||
|
||||
if (!languages.length) {
|
||||
// Negotiator expects plain object so we need to transform headers
|
||||
const negotiatorHeaders: Record<string, string> = {}
|
||||
headers().forEach((value, key) => (negotiatorHeaders[key] = value))
|
||||
const headersList = await headers()
|
||||
headersList.forEach((value, key) => (negotiatorHeaders[key] = value))
|
||||
// Use negotiator and intl-localematcher to get best locale
|
||||
languages = new Negotiator({ headers: negotiatorHeaders }).languages()
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "~5.8.3",
|
||||
"react-tooltip": "~5.29.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
|
||||
@ -112,7 +112,7 @@ export interface AppInfo {
|
||||
default_language: Locale
|
||||
copyright?: string
|
||||
privacy_policy?: string
|
||||
disable_session_same_site?: boolean
|
||||
disable_session_same_site?: boolean
|
||||
}
|
||||
|
||||
export enum Resolution {
|
||||
|
||||
Reference in New Issue
Block a user