refactor: update installed app component to handle missing params and improve type safety (#27331)
This commit is contained in:
@@ -3,7 +3,6 @@ import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
@@ -149,7 +148,7 @@ const ChatWrapper = () => {
|
||||
)
|
||||
}, [chatList, handleNewConversationCompleted, handleSend, currentConversationId, currentConversationInputs, newConversationInputs, isInstalledApp, appId])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
|
||||
doSend(editedQuestion ? editedQuestion.message : question.content,
|
||||
|
||||
@@ -3,7 +3,6 @@ import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
@@ -147,7 +146,7 @@ const ChatWrapper = () => {
|
||||
)
|
||||
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
|
||||
doSend(editedQuestion ? editedQuestion.message : question.content,
|
||||
|
||||
@@ -85,7 +85,7 @@ export type OnSend = {
|
||||
(message: string, files: FileEntity[] | undefined, isRegenerate: boolean, lastAnswer?: ChatItem | null): void
|
||||
}
|
||||
|
||||
export type OnRegenerate = (chatItem: ChatItem) => void
|
||||
export type OnRegenerate = (chatItem: ChatItem, editedQuestion?: { message: string; files?: FileEntity[] }) => void
|
||||
|
||||
export type Callback = {
|
||||
onSuccess: () => void
|
||||
|
||||
@@ -32,6 +32,7 @@ const meta = {
|
||||
},
|
||||
args: {
|
||||
show: false,
|
||||
children: null,
|
||||
},
|
||||
} satisfies Meta<typeof ContentDialog>
|
||||
|
||||
@@ -92,6 +93,9 @@ const DemoWrapper = (props: Props) => {
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: null,
|
||||
},
|
||||
render: args => <DemoWrapper {...args} />,
|
||||
}
|
||||
|
||||
@@ -99,6 +103,7 @@ export const NarrowPanel: Story = {
|
||||
render: args => <DemoWrapper {...args} />,
|
||||
args: {
|
||||
className: 'max-w-[420px]',
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import TimePicker from './index'
|
||||
import dayjs from '../utils/dayjs'
|
||||
import { isDayjsObject } from '../utils/dayjs'
|
||||
import type { TimePickerProps } from '../types'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
@@ -30,9 +31,10 @@ jest.mock('./options', () => () => <div data-testid="time-options" />)
|
||||
jest.mock('./header', () => () => <div data-testid="time-header" />)
|
||||
|
||||
describe('TimePicker', () => {
|
||||
const baseProps = {
|
||||
const baseProps: Pick<TimePickerProps, 'onChange' | 'onClear' | 'value'> = {
|
||||
onChange: jest.fn(),
|
||||
onClear: jest.fn(),
|
||||
value: undefined,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -150,7 +150,7 @@ export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptio
|
||||
|
||||
if (format) {
|
||||
const parsedWithFormat = tzName
|
||||
? dayjs.tz(trimmed, format, tzName, true)
|
||||
? dayjs(trimmed, format, true).tz(tzName, true)
|
||||
: dayjs(trimmed, format, true)
|
||||
if (parsedWithFormat.isValid())
|
||||
return parsedWithFormat
|
||||
@@ -191,7 +191,7 @@ export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptio
|
||||
const candidateFormats = formats ?? COMMON_PARSE_FORMATS
|
||||
for (const fmt of candidateFormats) {
|
||||
const parsed = tzName
|
||||
? dayjs.tz(trimmed, fmt, tzName, true)
|
||||
? dayjs(trimmed, fmt, true).tz(tzName, true)
|
||||
: dayjs(trimmed, fmt, true)
|
||||
if (parsed.isValid())
|
||||
return parsed
|
||||
|
||||
@@ -47,6 +47,7 @@ const meta = {
|
||||
args: {
|
||||
title: 'Manage API Keys',
|
||||
show: false,
|
||||
children: null,
|
||||
},
|
||||
} satisfies Meta<typeof Dialog>
|
||||
|
||||
@@ -102,6 +103,7 @@ export const Default: Story = {
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
children: null,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -110,6 +112,7 @@ export const WithoutFooter: Story = {
|
||||
args: {
|
||||
footer: undefined,
|
||||
title: 'Read-only summary',
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
@@ -140,6 +143,7 @@ export const CustomStyling: Story = {
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
@@ -42,7 +42,7 @@ export type FormOption = {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any>
|
||||
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any, any, any>
|
||||
|
||||
export type FormSchema = {
|
||||
type: FormTypeEnum
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChatContext } from '../chat/chat/context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const hasEndThink = (children: any): boolean => {
|
||||
if (typeof children === 'string')
|
||||
@@ -40,7 +41,7 @@ const useThinkTimer = (children: any) => {
|
||||
const [startTime] = useState(() => Date.now())
|
||||
const [elapsedTime, setElapsedTime] = useState(0)
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isComplete) return
|
||||
@@ -63,16 +64,26 @@ const useThinkTimer = (children: any) => {
|
||||
return { elapsedTime, isComplete }
|
||||
}
|
||||
|
||||
const ThinkBlock = ({ children, ...props }: React.ComponentProps<'details'>) => {
|
||||
type ThinkBlockProps = React.ComponentProps<'details'> & {
|
||||
'data-think'?: boolean
|
||||
}
|
||||
|
||||
const ThinkBlock = ({ children, ...props }: ThinkBlockProps) => {
|
||||
const { elapsedTime, isComplete } = useThinkTimer(children)
|
||||
const displayContent = removeEndThink(children)
|
||||
const { t } = useTranslation()
|
||||
const { 'data-think': isThink = false, className, open, ...rest } = props
|
||||
|
||||
if (!(props['data-think'] ?? false))
|
||||
if (!isThink)
|
||||
return (<details {...props}>{children}</details>)
|
||||
|
||||
return (
|
||||
<details {...(!isComplete && { open: true })} className="group">
|
||||
<details
|
||||
{...rest}
|
||||
data-think={isThink}
|
||||
className={cn('group', className)}
|
||||
open={isComplete ? open : true}
|
||||
>
|
||||
<summary className="flex cursor-pointer select-none list-none items-center whitespace-nowrap pl-2 font-bold text-text-secondary">
|
||||
<div className="flex shrink-0 items-center">
|
||||
<svg
|
||||
|
||||
@@ -45,6 +45,7 @@ const meta = {
|
||||
hideCloseBtn: false,
|
||||
onClose: () => console.log('close'),
|
||||
onConfirm: () => console.log('confirm'),
|
||||
children: null,
|
||||
},
|
||||
} satisfies Meta<typeof ModalLikeWrap>
|
||||
|
||||
@@ -68,6 +69,9 @@ export const Default: Story = {
|
||||
<BaseContent />
|
||||
</ModalLikeWrap>
|
||||
),
|
||||
args: {
|
||||
children: null,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithBackLink: Story = {
|
||||
@@ -90,6 +94,7 @@ export const WithBackLink: Story = {
|
||||
),
|
||||
args: {
|
||||
title: 'Select metadata type',
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
@@ -114,6 +119,7 @@ export const CustomWidth: Story = {
|
||||
),
|
||||
args: {
|
||||
title: 'Advanced configuration',
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react'
|
||||
import { Fragment, cloneElement, useRef } from 'react'
|
||||
import { Fragment, cloneElement, isValidElement, useRef } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type HtmlContentProps = {
|
||||
@@ -103,15 +103,17 @@ export default function CustomPopover({
|
||||
})
|
||||
}
|
||||
>
|
||||
{cloneElement(htmlContent as React.ReactElement, {
|
||||
open,
|
||||
onClose: close,
|
||||
...(manualClose
|
||||
? {
|
||||
onClick: close,
|
||||
}
|
||||
: {}),
|
||||
})}
|
||||
{isValidElement(htmlContent)
|
||||
? cloneElement(htmlContent as React.ReactElement<HtmlContentProps>, {
|
||||
open,
|
||||
onClose: close,
|
||||
...(manualClose
|
||||
? {
|
||||
onClick: close,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: htmlContent}
|
||||
</div>
|
||||
)}
|
||||
</PopoverPanel>
|
||||
|
||||
@@ -125,7 +125,7 @@ export const PortalToFollowElemTrigger = (
|
||||
children,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.HTMLProps<HTMLElement> & { ref?: React.RefObject<HTMLElement>, asChild?: boolean },
|
||||
}: React.HTMLProps<HTMLElement> & { ref?: React.RefObject<HTMLElement | null>, asChild?: boolean },
|
||||
) => {
|
||||
const context = usePortalToFollowElemContext()
|
||||
const childrenRef = (children as any).props?.ref
|
||||
@@ -133,12 +133,13 @@ export const PortalToFollowElemTrigger = (
|
||||
|
||||
// `asChild` allows the user to pass any element as the anchor
|
||||
if (asChild && React.isValidElement(children)) {
|
||||
const childProps = (children.props ?? {}) as Record<string, unknown>
|
||||
return React.cloneElement(
|
||||
children,
|
||||
context.getReferenceProps({
|
||||
ref,
|
||||
...props,
|
||||
...children.props,
|
||||
...childProps,
|
||||
'data-state': context.open ? 'open' : 'closed',
|
||||
} as React.HTMLProps<HTMLElement>),
|
||||
)
|
||||
@@ -164,7 +165,7 @@ export const PortalToFollowElemContent = (
|
||||
style,
|
||||
...props
|
||||
}: React.HTMLProps<HTMLDivElement> & {
|
||||
ref?: React.RefObject<HTMLDivElement>;
|
||||
ref?: React.RefObject<HTMLDivElement | null>;
|
||||
},
|
||||
) => {
|
||||
const context = usePortalToFollowElemContext()
|
||||
|
||||
@@ -35,7 +35,7 @@ import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
|
||||
import type { CustomTextNode } from './plugins/custom-text/node'
|
||||
import { registerLexicalTextEntity } from './utils'
|
||||
|
||||
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
|
||||
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement | null>, boolean]
|
||||
export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [editor] = useLexicalComposerContext()
|
||||
@@ -110,7 +110,7 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
|
||||
return [ref, isSelected]
|
||||
}
|
||||
|
||||
export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
|
||||
export type UseTriggerHandler = () => [RefObject<HTMLDivElement | null>, boolean, Dispatch<SetStateAction<boolean>>]
|
||||
export const useTrigger: UseTriggerHandler = () => {
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
@@ -8,7 +9,7 @@ const Placeholder = ({
|
||||
className,
|
||||
}: {
|
||||
compact?: boolean
|
||||
value?: string | JSX.Element
|
||||
value?: ReactNode
|
||||
className?: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -14,13 +14,19 @@ export const convertToMp3 = (recorder: any) => {
|
||||
const { channels, sampleRate } = wav
|
||||
const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128)
|
||||
const result = recorder.getChannelData()
|
||||
const buffer = []
|
||||
const buffer: BlobPart[] = []
|
||||
|
||||
const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2)
|
||||
const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2)
|
||||
const remaining = leftData.length + (rightData ? rightData.length : 0)
|
||||
|
||||
const maxSamples = 1152
|
||||
const toArrayBuffer = (bytes: Int8Array) => {
|
||||
const arrayBuffer = new ArrayBuffer(bytes.length)
|
||||
new Uint8Array(arrayBuffer).set(bytes)
|
||||
return arrayBuffer
|
||||
}
|
||||
|
||||
for (let i = 0; i < remaining; i += maxSamples) {
|
||||
const left = leftData.subarray(i, i + maxSamples)
|
||||
let right = null
|
||||
@@ -35,13 +41,13 @@ export const convertToMp3 = (recorder: any) => {
|
||||
}
|
||||
|
||||
if (mp3buf.length > 0)
|
||||
buffer.push(mp3buf)
|
||||
buffer.push(toArrayBuffer(mp3buf))
|
||||
}
|
||||
|
||||
const enc = mp3enc.flush()
|
||||
|
||||
if (enc.length > 0)
|
||||
buffer.push(enc)
|
||||
buffer.push(toArrayBuffer(enc))
|
||||
|
||||
return new Blob(buffer, { type: 'audio/mp3' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user