refactor: update installed app component to handle missing params and improve type safety (#27331)

This commit is contained in:
GuanMu
2025-10-27 14:38:58 +08:00
committed by GitHub
parent f06025a342
commit 43bcf40f80
49 changed files with 531 additions and 302 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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: {

View File

@@ -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(() => {

View File

@@ -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

View File

@@ -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: {

View File

@@ -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

View File

@@ -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

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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' })
}