This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
// Mock for context-block plugin to avoid circular dependency in Storybook
export const ContextBlockNode = null
export const ContextBlockReplacementBlock = null
export default null

View File

@@ -0,0 +1,4 @@
// Mock for history-block plugin to avoid circular dependency in Storybook
export const HistoryBlockNode = null
export const HistoryBlockReplacementBlock = null
export default null

View File

@@ -0,0 +1,4 @@
// Mock for query-block plugin to avoid circular dependency in Storybook
export const QueryBlockNode = null
export const QueryBlockReplacementBlock = null
export default null

View File

@@ -0,0 +1,45 @@
import type { StorybookConfig } from '@storybook/nextjs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const storybookDir = path.dirname(fileURLToPath(import.meta.url))
const config: StorybookConfig = {
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-docs',
'@chromatic-com/storybook',
],
framework: {
name: '@storybook/nextjs',
options: {
builder: {
useSWC: true,
lazyCompilation: false,
},
nextConfigPath: undefined,
},
},
staticDirs: ['../public'],
core: {
disableWhatsNewNotifications: true,
},
docs: {
defaultName: 'Documentation',
},
webpackFinal: async (config) => {
// Add alias to mock problematic modules with circular dependencies
config.resolve = config.resolve || {}
config.resolve.alias = {
...config.resolve.alias,
// Mock the plugin index files to avoid circular dependencies
[path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/context-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/context-block.tsx'),
[path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/history-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/history-block.tsx'),
[path.resolve(storybookDir, '../app/components/base/prompt-editor/plugins/query-block/index.tsx')]: path.resolve(storybookDir, '__mocks__/query-block.tsx'),
}
return config
},
}
export default config

View File

@@ -0,0 +1,56 @@
import type { Preview } from '@storybook/react'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import I18N from '../app/components/i18n'
import { ToastProvider } from '../app/components/base/toast'
import '../app/styles/globals.css'
import '../app/styles/markdown.scss'
import './storybook.css'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
export const decorators = [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-theme',
}),
(Story) => {
return (
<QueryClientProvider client={queryClient}>
<I18N locale="en-US">
<ToastProvider>
<Story />
</ToastProvider>
</I18N>
</QueryClientProvider>
)
},
]
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
toc: true,
},
},
tags: ['autodocs'],
}
export default preview

View File

@@ -0,0 +1,6 @@
html,
body {
max-width: unset;
overflow: auto;
user-select: text;
}

View File

@@ -0,0 +1,64 @@
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
type PlayerCallback = ((event: string) => void) | null
class MockAudioPlayer {
private callback: PlayerCallback = null
private finishTimer?: ReturnType<typeof setTimeout>
public setCallback(callback: PlayerCallback) {
this.callback = callback
}
public playAudio() {
this.clearTimer()
this.callback?.('play')
this.finishTimer = setTimeout(() => {
this.callback?.('ended')
}, 2000)
}
public pauseAudio() {
this.clearTimer()
this.callback?.('paused')
}
private clearTimer() {
if (this.finishTimer)
clearTimeout(this.finishTimer)
}
}
class MockAudioPlayerManager {
private readonly player = new MockAudioPlayer()
public getAudioPlayer(
_url: string,
_isPublic: boolean,
_id: string | undefined,
_msgContent: string | null | undefined,
_voice: string | undefined,
callback: PlayerCallback,
) {
this.player.setCallback(callback)
return this.player
}
public resetMsgId() {
// No-op for the mock
}
}
export const ensureMockAudioManager = () => {
const managerAny = AudioPlayerManager as unknown as {
getInstance: () => AudioPlayerManager
__isStorybookMockInstalled?: boolean
}
if (managerAny.__isStorybookMockInstalled)
return
const mock = new MockAudioPlayerManager()
managerAny.getInstance = () => mock as unknown as AudioPlayerManager
managerAny.__isStorybookMockInstalled = true
}

View File

@@ -0,0 +1,83 @@
import { useState } from 'react'
import type { ReactNode } from 'react'
import { useStore } from '@tanstack/react-form'
import { useAppForm } from '@/app/components/base/form'
type UseAppFormOptions = Parameters<typeof useAppForm>[0]
type AppFormInstance = ReturnType<typeof useAppForm>
type FormStoryWrapperProps = {
options?: UseAppFormOptions
children: (form: AppFormInstance) => ReactNode
title?: string
subtitle?: string
}
export const FormStoryWrapper = ({
options,
children,
title,
subtitle,
}: FormStoryWrapperProps) => {
const [lastSubmitted, setLastSubmitted] = useState<unknown>(null)
const [submitCount, setSubmitCount] = useState(0)
const form = useAppForm({
...options,
onSubmit: (context) => {
setSubmitCount(count => count + 1)
setLastSubmitted(context.value)
options?.onSubmit?.(context)
},
})
const values = useStore(form.store, state => state.values)
const isSubmitting = useStore(form.store, state => state.isSubmitting)
const canSubmit = useStore(form.store, state => state.canSubmit)
return (
<div className="flex flex-col gap-6 px-6 md:flex-row md:px-10">
<div className="flex-1 space-y-4">
{(title || subtitle) && (
<header className="space-y-1">
{title && <h3 className="text-lg font-semibold text-text-primary">{title}</h3>}
{subtitle && <p className="text-sm text-text-tertiary">{subtitle}</p>}
</header>
)}
{children(form)}
</div>
<aside className="w-full max-w-sm rounded-xl border border-divider-subtle bg-components-panel-bg p-4 text-xs text-text-secondary shadow-sm">
<div className="flex items-center justify-between text-[11px] uppercase tracking-wide text-text-tertiary">
<span>Form State</span>
<span>{submitCount} submit{submitCount === 1 ? '' : 's'}</span>
</div>
<dl className="mt-2 space-y-1">
<div className="flex items-center justify-between rounded-md bg-components-button-tertiary-bg px-2 py-1">
<dt className="font-medium text-text-secondary">isSubmitting</dt>
<dd className="font-mono text-[11px] text-text-primary">{String(isSubmitting)}</dd>
</div>
<div className="flex items-center justify-between rounded-md bg-components-button-tertiary-bg px-2 py-1">
<dt className="font-medium text-text-secondary">canSubmit</dt>
<dd className="font-mono text-[11px] text-text-primary">{String(canSubmit)}</dd>
</div>
</dl>
<div className="mt-3 space-y-2">
<div>
<div className="mb-1 font-medium text-text-secondary">Current Values</div>
<pre className="max-h-48 overflow-auto rounded-md bg-background-default-subtle p-3 font-mono text-[11px] leading-tight text-text-primary">
{JSON.stringify(values, null, 2)}
</pre>
</div>
<div>
<div className="mb-1 font-medium text-text-secondary">Last Submission</div>
<pre className="max-h-40 overflow-auto rounded-md bg-background-default-subtle p-3 font-mono text-[11px] leading-tight text-text-primary">
{lastSubmitted ? JSON.stringify(lastSubmitted, null, 2) : '—'}
</pre>
</div>
</div>
</aside>
</div>
)
}
export type FormStoryRender = (form: AppFormInstance) => ReactNode