feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @type { import('storybook-react-rspack').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: 'storybook-react-rspack',
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -0,0 +1,18 @@
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# chat-uikit
ChatArea UI组件
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mitt from 'mitt';
import {
renderHook,
type WrapperComponent,
} from '@testing-library/react-hooks';
import {
UIKitEvents,
UIKitEventContext,
type UIKitEventMap,
} from '@coze-common/chat-uikit-shared';
import { useObserveCardContainer } from '../../src/hooks/use-observe-card-container';
const disconnectFn = vi.fn();
const initFn = vi.fn();
const observeFn = vi.fn();
const ResizeObserverMock = vi.fn((fn: any) => {
initFn();
fn();
return {
disconnect: disconnectFn,
observe: observeFn,
takeRecords: vi.fn(),
unobserve: vi.fn(),
};
});
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
vi.useFakeTimers();
describe('use-observe-card', () => {
it('should call correctly', () => {
const eventCenter = mitt<UIKitEventMap>();
const onResize = vi.fn();
const wrapper: WrapperComponent<{
children: any;
}> = ({ children }) => (
<UIKitEventContext.Provider value={eventCenter}>
{children}
</UIKitEventContext.Provider>
);
renderHook(
() =>
useObserveCardContainer({
messageId: '123',
onResize,
cardContainerRef: { current: 12313 } as any,
}),
{
wrapper,
},
);
eventCenter.emit(UIKitEvents.AFTER_CARD_RENDER, { messageId: '123' });
vi.runAllTimers();
expect(observeFn).toHaveBeenCalledOnce();
expect(onResize).toHaveBeenCalledOnce();
expect(disconnectFn).toHaveBeenCalledOnce();
expect(initFn).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { it, expect } from 'vitest';
import { getFileExtensionAndName } from '../../src/utils/file-name';
it('should get file extension by xxx.extension case', () => {
const fileName = '《史蒂夫·乔布斯传》官方正式中文版电子书.pdf';
const { nameWithoutExtension, extension } = getFileExtensionAndName(fileName);
expect(extension).toBe('.pdf');
expect(nameWithoutExtension).toBe('《史蒂夫·乔布斯传》官方正式中文版电子书');
});
it('not get file extension by xxx case', () => {
const fileName = 'Visual Studio Code';
const { nameWithoutExtension, extension } = getFileExtensionAndName(fileName);
expect(extension).toBe('');
expect(nameWithoutExtension).toBe('Visual Studio Code');
});

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getImageDisplayAttribute } from '../../src/utils/image/get-image-display-attribute';
// 测试套件
describe('getImageDisplayAttribute', () => {
// 测试用例:长横图
it('should return cover attributes for a wide image', () => {
const contentWidth = 500;
const result = getImageDisplayAttribute(600, 100, contentWidth);
expect(result).toEqual({
displayHeight: 120,
displayWidth: contentWidth,
isCover: true,
});
});
// 测试用例:长竖图
it('should return cover attributes for a tall image', () => {
const contentWidth = 500;
const result = getImageDisplayAttribute(100, 600, contentWidth);
expect(result).toEqual({
displayHeight: 240,
displayWidth: 120,
isCover: true,
});
});
// 测试用例:等比展示图
it('should return proportional attributes for an image', () => {
const contentWidth = 500;
const result = getImageDisplayAttribute(240, 240, contentWidth);
expect(result).toEqual({
displayHeight: 240,
displayWidth: 240,
isCover: false,
});
});
// 测试用例:中长横图
it('should return proportional attributes for a medium-wide image', () => {
const contentWidth = 500;
const result = getImageDisplayAttribute(500, 250, contentWidth);
expect(result).toEqual({
displayWidth: 480,
displayHeight: 240,
isCover: false,
});
});
// 测试用例:小尺寸图
it('should return actual dimensions for a small image', () => {
const contentWidth = 500;
const result = getImageDisplayAttribute(200, 150, contentWidth);
expect(result).toEqual({
displayHeight: 150,
displayWidth: 200,
isCover: false,
});
});
// ...更多测试用例
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,12 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {
'@coze-arch/no-deep-relative-import': 'off',
'@coze-arch/no-batch-import-or-export': 'off',
'no-restricted-syntax': 'off',
'no-restricted-imports': 'off',
},
});

View File

@@ -0,0 +1,100 @@
{
"name": "@coze-common/chat-uikit",
"version": "0.0.1",
"description": "@coze-common/chat-uikit",
"license": "Apache-2.0",
"author": "liushuoyan@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"types": "./src/index.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "exit 0",
"dev": "NODE_ENV=development rspack serve",
"dev:storybook": "storybook dev -p 6006",
"lint": "eslint ./ --cache",
"lint:type": "tsc -p tsconfig.json --noEmit",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test --coverage",
"test:watch": "vitest --passWithNoTests"
},
"dependencies": {
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-md-box-adapter": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-common/chat-area-utils": "workspace:*",
"@coze-common/chat-core": "workspace:*",
"@coze-common/chat-hooks": "workspace:*",
"@coze-common/chat-uikit-shared": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"@douyinfe/semi-illustrations": "^2.36.0",
"@douyinfe/semi-ui": "~2.72.3",
"bowser": "2.11.0",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"rc-textarea": "^1.6.3",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-error-boundary": "^4.0.9",
"stylelint": "^15.11.0"
},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@rollup/plugin-commonjs": "^24.0.0",
"@rollup/plugin-json": "~6.0.0",
"@rollup/plugin-node-resolve": "~15.0.1",
"@rollup/plugin-replace": "^4.0.0",
"@rspack/cli": "0.4.0",
"@rspack/core": "0.4.0",
"@rspack/plugin-react-refresh": "0.4.0",
"@storybook/addon-essentials": "^7.6.7",
"@storybook/addon-interactions": "^7.6.7",
"@storybook/addon-links": "^7.6.7",
"@storybook/addon-onboarding": "^1.0.10",
"@storybook/blocks": "^7.6.7",
"@storybook/react": "^7.6.7",
"@storybook/react-vite": "^7.6.7",
"@storybook/test": "^7.6.7",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.3.35",
"@swc/helpers": "^0.4.12",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/lodash-es": "^4.17.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"ahooks": "^3.7.8",
"autoprefixer": "^10.4.16",
"file-loader": "^6.2.0",
"less-loader": "~11.1.3",
"postcss": "^8.4.32",
"postcss-loader": "^7.3.3",
"react-router-dom": "^6.11.1",
"rollup": "^4.9.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-node-externals": "^6.1.2",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-ts": "^3.1.1",
"storybook": "^7.6.7",
"storybook-builder-rspack": "~7.0.0-rc.25",
"storybook-react-rspack": "~7.0.0-rc.25",
"tailwindcss": "~3.3.3",
"vitest": "~3.0.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99984C1 3.15889 2.49238 1.6665 4.33333 1.6665H20.3096C20.7517 1.6665 21.1756 1.8421 21.4882 2.15466L30.5118 11.1783C30.8244 11.4909 31 11.9148 31 12.3569V34.9998C31 36.8408 29.5076 38.3332 27.6667 38.3332H4.33333C2.49238 38.3332 1 36.8408 1 34.9998V4.99984Z" fill="#C6C6CD"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.27006C21 2.04733 21.2693 1.93579 21.4268 2.09328L30.5732 11.2397C30.7307 11.3972 30.6192 11.6665 30.3964 11.6665H24.3333C22.4924 11.6665 21 10.1741 21 8.33317V2.27006Z" fill="#A7A7B0"/>
<path id="&#229;&#144;&#136;&#229;&#185;&#182;&#229;&#189;&#162;&#231;&#138;&#182;" d="M21.7169 15.8335C22.2429 15.8335 22.6693 16.2532 22.6693 16.771V29.896C22.6693 30.4138 22.2429 30.8335 21.7169 30.8335H10.2883C9.76233 30.8335 9.33594 30.4138 9.33594 29.896V16.771C9.33594 16.2532 9.76233 15.8335 10.2883 15.8335H21.7169ZM17.9074 26.146H14.0978V29.4272H17.9074V26.146ZM16.955 27.0835V28.1683H15.0502V27.0835H16.955ZM17.9513 19.5835H16.0026V21.4585H14.0978V23.3335H16.0026V25.2085H17.9513V23.3335H16.0465V21.4585H17.9513V19.5835ZM16.0026 17.7085H14.0978V19.5835H16.0026V17.7085Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
<path id="&#229;&#144;&#136;&#229;&#185;&#182;&#229;&#189;&#162;&#231;&#138;&#182;" d="M21.7169 15.8335C22.2429 15.8335 22.6693 16.2532 22.6693 16.771V29.896C22.6693 30.4138 22.2429 30.8335 21.7169 30.8335H10.2883C9.76233 30.8335 9.33594 30.4138 9.33594 29.896V16.771C9.33594 16.2532 9.76233 15.8335 10.2883 15.8335H21.7169ZM17.9074 26.146H14.0978V29.4272H17.9074V26.146ZM16.955 27.0835V28.1683H15.0502V27.0835H16.955ZM17.9513 19.5835H16.0026V21.4585H14.0978V23.3335H16.0026V25.2085H17.9513V23.3335H16.0465V21.4585H17.9513V19.5835ZM16.0026 17.7085H14.0978V19.5835H16.0026V17.7085Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#C6C6CD"/>
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#A7A7B0"/>
<path id="music_note" d="M13.0833 31.25C12.1667 31.25 11.3819 30.9236 10.7292 30.2708C10.0764 29.6181 9.75 28.8333 9.75 27.9167C9.75 27 10.0764 26.2153 10.7292 25.5625C11.3819 24.9097 12.1667 24.5833 13.0833 24.5833C13.4028 24.5833 13.6979 24.6215 13.9688 24.6979C14.2396 24.7743 14.5 24.8889 14.75 25.0417V17.5C14.75 16.8096 15.3096 16.25 16 16.25H21V18.75H17.0417C16.6965 18.75 16.4167 19.0298 16.4167 19.375V27.9167C16.4167 28.8333 16.0903 29.6181 15.4375 30.2708C14.7847 30.9236 14 31.25 13.0833 31.25Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#32A645"/>
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#258832"/>
<path id="music_note" d="M13.0833 31.25C12.1667 31.25 11.3819 30.9236 10.7292 30.2708C10.0764 29.6181 9.75 28.8333 9.75 27.9167C9.75 27 10.0764 26.2153 10.7292 25.5625C11.3819 24.9097 12.1667 24.5833 13.0833 24.5833C13.4028 24.5833 13.6979 24.6215 13.9688 24.6979C14.2396 24.7743 14.5 24.8889 14.75 25.0417V17.5C14.75 16.8096 15.3096 16.25 16 16.25H21V18.75H17.0417C16.6965 18.75 16.4167 19.0298 16.4167 19.375V27.9167C16.4167 28.8333 16.0903 29.6181 15.4375 30.2708C14.7847 30.9236 14 31.25 13.0833 31.25Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99984C1 3.15889 2.49238 1.6665 4.33333 1.6665H20.3096C20.7517 1.6665 21.1756 1.8421 21.4882 2.15466L30.5118 11.1783C30.8244 11.4909 31 11.9148 31 12.3569V34.9998C31 36.8408 29.5076 38.3332 27.6667 38.3332H4.33333C2.49238 38.3332 1 36.8408 1 34.9998V4.99984Z" fill="#C6C6CD"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.27006C21 2.04733 21.2693 1.93579 21.4268 2.09328L30.5732 11.2397C30.7307 11.3972 30.6192 11.6665 30.3964 11.6665H24.3333C22.4924 11.6665 21 10.1741 21 8.33317V2.27006Z" fill="#A7A7B0"/>
<path id="icon_file_code_nor" d="M13.1583 19.1273L8.94653 23.5605L13.1583 27.9936C13.3094 28.1527 13.3067 28.4078 13.1523 28.5635L13.1506 28.5652L12.5848 29.1289C12.4301 29.2831 12.1836 29.2796 12.033 29.1211L7.01788 23.8423C6.86904 23.6857 6.86904 23.4352 7.01788 23.2786L12.033 17.9998C12.1836 17.8414 12.4301 17.8379 12.5848 17.992L13.1506 18.5558C13.306 18.7105 13.3101 18.9657 13.1598 19.1257L13.1583 19.1273ZM23.1935 23.5605L19.2826 19.1273C19.1423 18.9683 19.1448 18.7131 19.2882 18.5574L19.2897 18.5558L19.8152 17.992C19.9588 17.8379 20.1877 17.8414 20.3275 17.9998L24.9844 23.2786C25.1226 23.4352 25.1226 23.6857 24.9844 23.8423L20.3275 29.1211C20.1877 29.2796 19.9588 29.2831 19.8152 29.1289L19.2897 28.5652C19.1455 28.4104 19.1416 28.1552 19.2812 27.9952L19.2826 27.9936L23.1935 23.5605ZM17.0448 15.1539L17.8645 15.2418C18.0882 15.2658 18.2487 15.4536 18.2228 15.6611L16.2662 31.1805C16.2401 31.3879 16.0378 31.5364 15.8143 31.5125L14.9946 31.4246C14.7708 31.4006 14.6104 31.2128 14.6363 31.0052L16.5928 15.4858C16.619 15.2785 16.8212 15.1299 17.0448 15.1539Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
<path id="icon_file_code_nor" d="M13.1583 19.1273L8.94653 23.5605L13.1583 27.9936C13.3094 28.1527 13.3067 28.4078 13.1523 28.5635L13.1506 28.5652L12.5848 29.1289C12.4301 29.2831 12.1836 29.2796 12.033 29.1211L7.01788 23.8423C6.86904 23.6857 6.86904 23.4352 7.01788 23.2786L12.033 17.9998C12.1836 17.8414 12.4301 17.8379 12.5848 17.992L13.1506 18.5558C13.306 18.7105 13.3101 18.9657 13.1598 19.1257L13.1583 19.1273ZM23.1935 23.5605L19.2826 19.1273C19.1423 18.9683 19.1448 18.7131 19.2882 18.5574L19.2897 18.5558L19.8152 17.992C19.9588 17.8379 20.1877 17.8414 20.3275 17.9998L24.9844 23.2786C25.1226 23.4352 25.1226 23.6857 24.9844 23.8423L20.3275 29.1211C20.1877 29.2796 19.9588 29.2831 19.8152 29.1289L19.2897 28.5652C19.1455 28.4104 19.1416 28.1552 19.2812 27.9952L19.2826 27.9936L23.1935 23.5605ZM17.0448 15.1539L17.8645 15.2418C18.0882 15.2658 18.2487 15.4536 18.2228 15.6611L16.2662 31.1805C16.2401 31.3879 16.0378 31.5364 15.8143 31.5125L14.9946 31.4246C14.7708 31.4006 14.6104 31.2128 14.6363 31.0052L16.5928 15.4858C16.619 15.2785 16.8212 15.1299 17.0448 15.1539Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#C6C6CD"/>
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#A7A7B0"/>
<path id="C" d="M16.1282 17.4213C17.6321 17.4213 18.8674 17.9108 19.8342 18.7523C20.3043 19.1577 20.7116 19.8654 20.9997 20.4667C21.247 20.9829 20.8541 21.544 20.2818 21.544H19.9622C19.6618 21.544 19.3856 21.3818 19.2195 21.1315C18.9874 20.7818 18.6288 20.2981 18.2766 20.0474C17.7037 19.6267 16.9696 19.4047 16.0744 19.4047C14.7317 19.4047 13.729 19.9135 13.0487 20.9079C12.4221 21.7875 12.0357 22.9957 12.0357 24.5638C12.0357 26.1701 12.4221 27.4288 13.0308 28.2893C13.6932 29.2072 14.7317 29.7673 16.1282 29.7673C17.0412 29.7673 17.7932 29.4367 18.3661 28.9777C18.7224 28.6756 19.0418 28.1042 19.2455 27.6822C19.3954 27.3717 19.7036 27.159 20.0483 27.159H20.4217C20.9677 27.159 21.3582 27.6737 21.1626 28.1835C20.8809 28.9174 20.4299 29.8634 19.8342 30.3767C18.7694 31.2941 17.9221 31.6579 16.1282 31.7062C14.0681 31.7617 12.4221 30.9283 11.3836 29.5132C10.4705 28.2893 10.0229 26.6448 10.0229 24.5795C10.0229 22.5525 10.4884 20.8888 11.4374 19.6267C12.5116 18.1733 14.0871 17.4213 16.1282 17.4213Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#35BD4B"/>
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#258832"/>
<path id="C" d="M16.1282 17.4213C17.6321 17.4213 18.8674 17.9108 19.8342 18.7523C20.3043 19.1577 20.7116 19.8654 20.9997 20.4667C21.247 20.9829 20.8541 21.544 20.2818 21.544H19.9622C19.6618 21.544 19.3856 21.3818 19.2195 21.1315C18.9874 20.7818 18.6288 20.2981 18.2766 20.0474C17.7037 19.6267 16.9696 19.4047 16.0744 19.4047C14.7317 19.4047 13.729 19.9135 13.0487 20.9079C12.4221 21.7875 12.0357 22.9957 12.0357 24.5638C12.0357 26.1701 12.4221 27.4288 13.0308 28.2893C13.6932 29.2072 14.7317 29.7673 16.1282 29.7673C17.0412 29.7673 17.7932 29.4367 18.3661 28.9777C18.7224 28.6756 19.0418 28.1042 19.2455 27.6822C19.3954 27.3717 19.7036 27.159 20.0483 27.159H20.4217C20.9677 27.159 21.3582 27.6737 21.1626 28.1835C20.8809 28.9174 20.4299 29.8634 19.8342 30.3767C18.7694 31.2941 17.9221 31.6579 16.1282 31.7062C14.0681 31.7617 12.4221 30.9283 11.3836 29.5132C10.4705 28.2893 10.0229 26.6448 10.0229 24.5795C10.0229 22.5525 10.4884 20.8888 11.4374 19.6267C12.5116 18.1733 14.0871 17.4213 16.1282 17.4213Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2530" opacity="0.9" d="M1 4.99984C1 3.15889 2.49238 1.6665 4.33333 1.6665H20.3096C20.7517 1.6665 21.1756 1.8421 21.4882 2.15466L30.5118 11.1783C30.8244 11.4909 31 11.9148 31 12.3569V34.9998C31 36.8408 29.5076 38.3332 27.6667 38.3332H4.33333C2.49238 38.3332 1 36.8408 1 34.9998V4.99984Z" fill="#C6C6CD"/>
<path id="Rectangle 2531" opacity="0.6" d="M21 2.27006C21 2.04733 21.2693 1.93579 21.4268 2.09328L30.5732 11.2397C30.7307 11.3972 30.6192 11.6665 30.3964 11.6665H24.3333C22.4924 11.6665 21 10.1741 21 8.33317V2.27006Z" fill="#A7A7B0"/>
<path id="&#239;&#188;&#159;" d="M19.817 16.4407C18.9075 15.579 17.7137 15.1514 16.2292 15.1514C14.5615 15.1514 13.2546 15.686 12.3077 16.775C11.5055 17.7108 11.0734 18.6077 11.0116 20.0217C11.0101 20.055 11.0089 20.1162 11.0079 20.187C11.0045 20.4179 11.1919 20.6059 11.4229 20.6059H12.408C12.6371 20.6059 12.8227 20.4209 12.8264 20.1918C12.8276 20.1228 12.8289 20.0633 12.8305 20.0315C12.8789 19.0547 13.1337 18.5436 13.5885 17.9752C14.1626 17.2098 15.0048 16.8332 16.161 16.8332C17.1928 16.8332 17.9663 17.0963 18.5003 17.6303C19.0092 18.1605 19.2746 18.8823 19.2746 19.7877C19.2746 20.4058 19.0506 20.9945 18.5952 21.58C18.4502 21.7613 18.2412 21.977 17.5685 22.6498C16.5057 23.579 15.8493 24.3349 15.5025 25.078C15.2174 25.648 15.0721 26.2959 15.0721 26.9923V27.462C15.0721 27.6921 15.2586 27.8786 15.4888 27.8786H16.4963C16.7264 27.8786 16.913 27.6921 16.913 27.462V26.9923C16.913 26.4021 17.0583 25.8728 17.3593 25.3568C17.5847 24.9736 17.8742 24.673 18.3788 24.2315C19.3906 23.3161 19.9519 22.7708 20.2033 22.4509C20.8072 21.6538 21.1155 20.7409 21.1155 19.7423C21.1155 18.3641 20.6829 17.2584 19.817 16.4407ZM15.5115 29.6968C15.2814 29.6968 15.0948 29.8834 15.0948 30.1135V31.0983C15.0948 31.3285 15.2814 31.515 15.5115 31.515H16.4963C16.7265 31.515 16.913 31.3285 16.913 31.0983V30.1135C16.913 29.8834 16.7265 29.6968 16.4963 29.6968H15.5115Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2530" opacity="0.9" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#8F959E"/>
<path id="Rectangle 2531" opacity="0.6" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#646A73"/>
<path id="&#239;&#188;&#159;" d="M19.817 16.4407C18.9075 15.579 17.7137 15.1514 16.2292 15.1514C14.5615 15.1514 13.2546 15.686 12.3077 16.775C11.5055 17.7108 11.0734 18.6077 11.0116 20.0217C11.0101 20.055 11.0089 20.1162 11.0079 20.187C11.0045 20.4179 11.1919 20.6059 11.4229 20.6059H12.408C12.6371 20.6059 12.8227 20.4209 12.8264 20.1918C12.8276 20.1228 12.8289 20.0633 12.8305 20.0315C12.8789 19.0547 13.1337 18.5436 13.5885 17.9752C14.1626 17.2098 15.0048 16.8332 16.161 16.8332C17.1928 16.8332 17.9663 17.0963 18.5003 17.6303C19.0092 18.1605 19.2746 18.8823 19.2746 19.7877C19.2746 20.4058 19.0506 20.9945 18.5952 21.58C18.4502 21.7613 18.2412 21.977 17.5685 22.6498C16.5057 23.579 15.8493 24.3349 15.5025 25.078C15.2174 25.648 15.0721 26.2959 15.0721 26.9923V27.462C15.0721 27.6921 15.2586 27.8786 15.4888 27.8786H16.4963C16.7264 27.8786 16.913 27.6921 16.913 27.462V26.9923C16.913 26.4021 17.0583 25.8728 17.3593 25.3568C17.5847 24.9736 17.8742 24.673 18.3788 24.2315C19.3906 23.3161 19.9519 22.7708 20.2033 22.4509C20.8072 21.6538 21.1155 20.7409 21.1155 19.7423C21.1155 18.3641 20.6829 17.2584 19.817 16.4407ZM15.5115 29.6968C15.2814 29.6968 15.0948 29.8834 15.0948 30.1135V31.0983C15.0948 31.3285 15.2814 31.515 15.5115 31.515H16.4963C16.7265 31.515 16.913 31.3285 16.913 31.0983V30.1135C16.913 29.8834 16.7265 29.6968 16.4963 29.6968H15.5115Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,8 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M4 4.99997C4 3.15902 5.19391 1.66664 6.66667 1.66664H19.4477C19.8013 1.66664 20.1405 1.84224 20.3905 2.1548L27.6095 11.1785C27.8595 11.491 28 11.915 28 12.357V35C28 36.8409 26.8061 38.3333 25.3333 38.3333H6.66667C5.19391 38.3333 4 36.8409 4 35V4.99997Z" fill="#336DF4"/>
<path id="Rectangle 2530" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#C6C6CD"/>
<path id="Rectangle 2531" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#A7A7B0"/>
<path id="icon_file_word_nor" d="M16.0074 20.4158L13.5499 29.51C13.52 29.6204 13.4199 29.697 13.3056 29.697H11.8581C11.7449 29.697 11.6455 29.6219 11.6147 29.513L8.2821 17.7463C8.24402 17.6119 8.32215 17.472 8.45662 17.4339C8.47905 17.4275 8.50226 17.4243 8.52557 17.4243H9.97719C10.0926 17.4243 10.1933 17.5023 10.2222 17.614L12.5937 26.7863L15.0564 17.6118C15.086 17.5012 15.1863 17.4243 15.3008 17.4243H16.7153C16.8299 17.4243 16.9302 17.5014 16.9598 17.6121L19.4067 26.784L21.7776 17.614C21.8064 17.5023 21.9072 17.4243 22.0226 17.4243H23.4742C23.6139 17.4243 23.7272 17.5376 23.7272 17.6774C23.7272 17.7007 23.724 17.7239 23.7177 17.7463L20.3851 29.513C20.3542 29.6219 20.2548 29.697 20.1416 29.697H18.6942C18.5797 29.697 18.4795 29.6202 18.4498 29.5096L16.0074 20.4158Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,8 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M4 4.99997C4 3.15902 5.19391 1.66664 6.66667 1.66664H19.4477C19.8013 1.66664 20.1405 1.84224 20.3905 2.1548L27.6095 11.1785C27.8595 11.491 28 11.915 28 12.357V35C28 36.8409 26.8061 38.3333 25.3333 38.3333H6.66667C5.19391 38.3333 4 36.8409 4 35V4.99997Z" fill="#336DF4"/>
<path id="Rectangle 2530" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#336DF4"/>
<path id="Rectangle 2531" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#0442D2"/>
<path id="icon_file_word_nor" d="M16.0074 20.4158L13.5499 29.51C13.52 29.6204 13.4199 29.697 13.3056 29.697H11.8581C11.7449 29.697 11.6455 29.6219 11.6147 29.513L8.2821 17.7463C8.24402 17.6119 8.32215 17.472 8.45662 17.4339C8.47905 17.4275 8.50226 17.4243 8.52557 17.4243H9.97719C10.0926 17.4243 10.1933 17.5023 10.2222 17.614L12.5937 26.7863L15.0564 17.6118C15.086 17.5012 15.1863 17.4243 15.3008 17.4243H16.7153C16.8299 17.4243 16.9302 17.5014 16.9598 17.6121L19.4067 26.784L21.7776 17.614C21.8064 17.5023 21.9072 17.4243 22.0226 17.4243H23.4742C23.6139 17.4243 23.7272 17.5376 23.7272 17.6774C23.7272 17.7007 23.724 17.7239 23.7177 17.7463L20.3851 29.513C20.3542 29.6219 20.2548 29.697 20.1416 29.697H18.6942C18.5797 29.697 18.4795 29.6202 18.4498 29.5096L16.0074 20.4158Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,10 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#C6C6CD"/>
<path id="Rectangle 2529" opacity="0.8" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#A7A7B0"/>
<g id="&#229;&#189;&#162;&#231;&#138;&#182;&#231;&#187;&#147;&#229;&#144;&#136;">
<path d="M9.95312 16.6666C9.03265 16.6666 8.28646 17.4128 8.28646 18.3333V18.6363C8.28646 19.5568 9.03265 20.303 9.95312 20.303H10.2562C11.1766 20.303 11.9228 19.5568 11.9228 18.6363V18.3333C11.9228 17.4128 11.1766 16.6666 10.2562 16.6666H9.95312Z" fill="white"/>
<path d="M23.8254 21.2663C24.4434 20.5989 25.5592 21.0362 25.5592 21.9457V30.8333C25.5592 31.2935 25.1861 31.6666 24.7259 31.6666L8.33494 31.6666C7.76879 31.6666 7.46033 31.0055 7.82405 30.5716L12.7 24.7554C13.3661 23.9609 14.5883 23.9609 15.2544 24.7554L17.792 27.7824L23.8254 21.2663Z" fill="white"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,10 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#FFC60A"/>
<path id="Rectangle 2529" opacity="0.8" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#D99904"/>
<g id="&#229;&#189;&#162;&#231;&#138;&#182;&#231;&#187;&#147;&#229;&#144;&#136;">
<path d="M9.95312 16.6666C9.03265 16.6666 8.28646 17.4128 8.28646 18.3333V18.6363C8.28646 19.5568 9.03265 20.303 9.95312 20.303H10.2562C11.1766 20.303 11.9228 19.5568 11.9228 18.6363V18.3333C11.9228 17.4128 11.1766 16.6666 10.2562 16.6666H9.95312Z" fill="white"/>
<path d="M23.8254 21.2663C24.4434 20.5989 25.5592 21.0362 25.5592 21.9457V30.8333C25.5592 31.2935 25.1861 31.6666 24.7259 31.6666L8.33494 31.6666C7.76879 31.6666 7.46033 31.0055 7.82405 30.5716L12.7 24.7554C13.3661 23.9609 14.5883 23.9609 15.2544 24.7554L17.792 27.7824L23.8254 21.2663Z" fill="white"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#C6C6CD"/>
<path id="Rectangle 2527" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#A7A7B0"/>
<path id="icon_file_pdf_nor" d="M25.2516 25.9184C24.8379 25.4341 23.9896 25.1986 22.6583 25.1986C21.8845 25.1986 20.8185 25.2181 19.7472 25.3771C16.821 23.2676 16.1352 21.0015 16.1352 21.0015C16.1352 21.0015 16.635 19.7471 16.6667 17.6982C16.6868 16.403 16.4814 15.437 15.9578 15.0218C15.7347 14.8449 15.4108 14.697 15.0848 14.697C14.8303 14.697 14.592 14.7707 14.3967 14.9115C12.8756 16.009 14.5363 21.1828 14.5808 21.3207C13.8629 23.0626 12.9586 24.9081 12.0279 26.5297C11.7255 27.0565 11.7255 27.0667 11.5223 27.3059C11.5223 27.3059 8.85954 28.6265 7.61092 30.0907C6.90544 30.9181 6.88254 31.4861 6.92 31.9118C6.98036 32.4223 7.63116 32.8788 8.28633 32.8788C8.3135 32.8788 8.34084 32.878 8.36762 32.8763C9.03349 32.8358 9.77415 32.6526 10.5983 31.873C11.195 31.3086 11.866 29.7758 12.7286 28.276C15.2034 27.5818 17.3814 27.0874 19.2066 26.8055C20.5451 27.5157 22.5366 28.3201 23.8922 28.3201C24.3469 28.3201 24.7128 28.2287 24.9795 28.0484C25.2986 27.8328 25.4341 27.5641 25.5183 27.0667C25.6025 26.5693 25.4853 26.1921 25.2516 25.9184ZM22.342 26.697C23.578 26.697 24.2472 26.9151 24.591 27.098C24.697 27.1544 24.7742 27.2089 24.8289 27.2543C24.732 27.3294 24.5414 27.4243 24.1971 27.4243C23.626 27.4243 22.8765 27.1823 21.9623 26.7036C22.0924 26.6992 22.219 26.697 22.342 26.697ZM15.2432 15.7931L15.2453 15.7879C15.4383 15.93 15.5284 16.9281 15.5103 17.5068C15.486 18.2834 15.4802 18.5835 15.3827 19.0606C15.1185 18.0659 15.0997 16.2779 15.2432 15.7931ZM15.3042 23.0606C15.907 24.0536 16.6715 25.0591 17.5328 25.827C15.8516 26.1874 14.4556 26.5181 13.4512 26.8696C14.5278 25.005 15.2335 23.2454 15.3042 23.0606ZM8.51592 31.4738C8.66804 31.2488 9.08367 30.8128 10.1377 29.9697C9.57203 31.2728 8.93672 31.4738 8.34612 31.7878C8.39069 31.6845 8.44678 31.5761 8.51592 31.4738Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#F54A45"/>
<path id="Rectangle 2527" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#C02A26"/>
<path id="icon_file_pdf_nor" d="M25.2516 25.9184C24.8379 25.4341 23.9896 25.1986 22.6583 25.1986C21.8845 25.1986 20.8185 25.2181 19.7472 25.3771C16.821 23.2676 16.1352 21.0015 16.1352 21.0015C16.1352 21.0015 16.635 19.7471 16.6667 17.6982C16.6868 16.403 16.4814 15.437 15.9578 15.0218C15.7347 14.8449 15.4108 14.697 15.0848 14.697C14.8303 14.697 14.592 14.7707 14.3967 14.9115C12.8756 16.009 14.5363 21.1828 14.5808 21.3207C13.8629 23.0626 12.9586 24.9081 12.0279 26.5297C11.7255 27.0565 11.7255 27.0667 11.5223 27.3059C11.5223 27.3059 8.85954 28.6265 7.61092 30.0907C6.90544 30.9181 6.88254 31.4861 6.92 31.9118C6.98036 32.4223 7.63116 32.8788 8.28633 32.8788C8.3135 32.8788 8.34084 32.878 8.36762 32.8763C9.03349 32.8358 9.77415 32.6526 10.5983 31.873C11.195 31.3086 11.866 29.7758 12.7286 28.276C15.2034 27.5818 17.3814 27.0874 19.2066 26.8055C20.5451 27.5157 22.5366 28.3201 23.8922 28.3201C24.3469 28.3201 24.7128 28.2287 24.9795 28.0484C25.2986 27.8328 25.4341 27.5641 25.5183 27.0667C25.6025 26.5693 25.4853 26.1921 25.2516 25.9184ZM22.342 26.697C23.578 26.697 24.2472 26.9151 24.591 27.098C24.697 27.1544 24.7742 27.2089 24.8289 27.2543C24.732 27.3294 24.5414 27.4243 24.1971 27.4243C23.626 27.4243 22.8765 27.1823 21.9623 26.7036C22.0924 26.6992 22.219 26.697 22.342 26.697ZM15.2432 15.7931L15.2453 15.7879C15.4383 15.93 15.5284 16.9281 15.5103 17.5068C15.486 18.2834 15.4802 18.5835 15.3827 19.0606C15.1185 18.0659 15.0997 16.2779 15.2432 15.7931ZM15.3042 23.0606C15.907 24.0536 16.6715 25.0591 17.5328 25.827C15.8516 26.1874 14.4556 26.5181 13.4512 26.8696C14.5278 25.005 15.2335 23.2454 15.3042 23.0606ZM8.51592 31.4738C8.66804 31.2488 9.08367 30.8128 10.1377 29.9697C9.57203 31.2728 8.93672 31.4738 8.34612 31.7878C8.39069 31.6845 8.44678 31.5761 8.51592 31.4738Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2530" d="M1 4.99984C1 3.15889 2.49238 1.6665 4.33333 1.6665H20.3096C20.7517 1.6665 21.1756 1.8421 21.4882 2.15466L30.5118 11.1783C30.8244 11.4909 31 11.9148 31 12.3569V34.9998C31 36.8408 29.5076 38.3332 27.6667 38.3332H4.33333C2.49238 38.3332 1 36.8408 1 34.9998V4.99984Z" fill="#C6C6CD"/>
<path id="Rectangle 2531" opacity="0.8" d="M21 2.27006C21 2.04733 21.2693 1.93579 21.4268 2.09328L30.5732 11.2397C30.7307 11.3972 30.6192 11.6665 30.3964 11.6665H24.3333C22.4924 11.6665 21 10.1741 21 8.33317V2.27006Z" fill="#A7A7B0"/>
<path id="Subtract" d="M13.3038 30.8251V24.8477H16.1402C16.8218 24.8477 17.5001 24.7878 18.1749 24.668C18.8621 24.546 19.479 24.3316 20.0243 24.0244C20.5793 23.7117 21.032 23.2837 21.3799 22.7429C21.7332 22.1938 21.9077 21.5053 21.9077 20.6814C21.9077 20.1383 21.8345 19.6179 21.688 19.1206C21.5371 18.6083 21.2705 18.1546 20.8902 17.7622C20.5082 17.3681 19.9947 17.0617 19.3529 16.8413C18.717 16.6229 17.92 16.5151 16.9605 16.5151H11.6886C11.5586 16.5151 11.4531 16.6206 11.4531 16.7506V30.8251C11.4531 30.9552 11.5586 31.0606 11.6886 31.0606H13.0683C13.1983 31.0606 13.3038 30.9552 13.3038 30.8251ZM17.8627 23.1217C17.3692 23.1978 16.8082 23.236 16.1801 23.236H13.3037V18.1268H16.5803C17.8243 18.1268 18.7172 18.3424 19.2601 18.7594C19.7912 19.1673 20.057 19.7679 20.057 20.5837C20.057 21.1479 19.9675 21.5968 19.7935 21.9308C19.6202 22.2634 19.3773 22.5236 19.062 22.7154C18.738 22.9123 18.3387 23.0483 17.8627 23.1217Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2530" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#FF811A"/>
<path id="Rectangle 2531" opacity="0.8" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#ED6D0C"/>
<path id="Subtract" d="M13.3038 30.8251V24.8477H16.1402C16.8218 24.8477 17.5001 24.7878 18.1749 24.668C18.8621 24.546 19.479 24.3316 20.0243 24.0244C20.5793 23.7117 21.032 23.2837 21.3799 22.7429C21.7332 22.1938 21.9077 21.5053 21.9077 20.6814C21.9077 20.1383 21.8345 19.6179 21.688 19.1206C21.5371 18.6083 21.2705 18.1546 20.8902 17.7622C20.5082 17.3681 19.9947 17.0617 19.3529 16.8413C18.717 16.6229 17.92 16.5151 16.9605 16.5151H11.6886C11.5586 16.5151 11.4531 16.6206 11.4531 16.7506V30.8251C11.4531 30.9552 11.5586 31.0606 11.6886 31.0606H13.0683C13.1983 31.0606 13.3038 30.9552 13.3038 30.8251ZM17.8627 23.1217C17.3692 23.1978 16.8082 23.236 16.1801 23.236H13.3037V18.1268H16.5803C17.8243 18.1268 18.7172 18.3424 19.2601 18.7594C19.7912 19.1673 20.057 19.7679 20.057 20.5837C20.057 21.1479 19.9675 21.5968 19.7935 21.9308C19.6202 22.2634 19.3773 22.5236 19.062 22.7154C18.738 22.9123 18.3387 23.0483 17.8627 23.1217Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#C6C6CD"/>
<path id="Rectangle 2527" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#A7A7B0"/>
<path id="Union" d="M16.9089 17.8787V30.3787C16.9089 30.5043 16.8072 30.606 16.6816 30.606H15.318C15.1925 30.606 15.0907 30.5043 15.0907 30.3787V17.8787H9.40891C9.28339 17.8787 9.18164 17.777 9.18164 17.6515V16.2878C9.18164 16.1623 9.28339 16.0606 9.40891 16.0606H22.5907C22.7163 16.0606 22.818 16.1623 22.818 16.2878V17.6515C22.818 17.777 22.7163 17.8787 22.5907 17.8787H16.9089Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#336DF4"/>
<path id="Rectangle 2527" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#0442D2"/>
<path id="Union" d="M16.9089 17.8787V30.3787C16.9089 30.5043 16.8072 30.606 16.6816 30.606H15.318C15.1925 30.606 15.0907 30.5043 15.0907 30.3787V17.8787H9.40891C9.28339 17.8787 9.18164 17.777 9.18164 17.6515V16.2878C9.18164 16.1623 9.28339 16.0606 9.40891 16.0606H22.5907C22.7163 16.0606 22.818 16.1623 22.818 16.2878V17.6515C22.818 17.777 22.7163 17.8787 22.5907 17.8787H16.9089Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99984C1 3.15889 2.49238 1.6665 4.33333 1.6665H20.3096C20.7517 1.6665 21.1756 1.8421 21.4882 2.15466L30.5118 11.1783C30.8244 11.4909 31 11.9148 31 12.3569V34.9998C31 36.8408 29.5076 38.3332 27.6667 38.3332H4.33333C2.49238 38.3332 1 36.8408 1 34.9998V4.99984Z" fill="#C6C6CD"/>
<path id="Rectangle 2527" opacity="0.7" d="M21 2.27006C21 2.04733 21.2693 1.93579 21.4268 2.09328L30.5732 11.2397C30.7307 11.3972 30.6192 11.6665 30.3964 11.6665H24.3333C22.4924 11.6665 21 10.1741 21 8.33317V2.27006Z" fill="#A7A7B0"/>
<path id="Union" d="M16.907 17.8787V30.3787C16.907 30.5042 16.8052 30.606 16.6797 30.606H15.3161C15.1905 30.606 15.0888 30.5042 15.0888 30.3787V17.8787H9.40696C9.28144 17.8787 9.17969 17.777 9.17969 17.6515V16.2878C9.17969 16.1623 9.28144 16.0605 9.40696 16.0605H22.5888C22.7143 16.0605 22.8161 16.1623 22.8161 16.2878V17.6515C22.8161 17.777 22.7143 17.8787 22.5888 17.8787H16.907Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
<path id="Rectangle 2527" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
<path id="Union" d="M16.907 17.8787V30.3787C16.907 30.5042 16.8052 30.606 16.6797 30.606H15.3161C15.1905 30.606 15.0888 30.5042 15.0888 30.3787V17.8787H9.40696C9.28144 17.8787 9.17969 17.777 9.17969 17.6515V16.2878C9.17969 16.1623 9.28144 16.0605 9.40696 16.0605H22.5888C22.7143 16.0605 22.8161 16.1623 22.8161 16.2878V17.6515C22.8161 17.777 22.7143 17.8787 22.5888 17.8787H16.907Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2530" opacity="0.9" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#C6C6CD"/>
<path id="Rectangle 2531" opacity="0.6" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#A7A7B0"/>
<path id="&#239;&#188;&#159;" d="M19.8136 16.4407C18.904 15.579 17.7103 15.1514 16.2258 15.1514C14.5581 15.1514 13.2511 15.686 12.3042 16.775C11.5021 17.7108 11.07 18.6077 11.0082 20.0217C11.0067 20.055 11.0055 20.1162 11.0044 20.187C11.001 20.4179 11.1884 20.6059 11.4195 20.6059H12.4045C12.6337 20.6059 12.8192 20.4209 12.823 20.1918C12.8242 20.1228 12.8255 20.0633 12.8271 20.0315C12.8755 19.0547 13.1303 18.5436 13.585 17.9752C14.1592 17.2098 15.0014 16.8332 16.1576 16.8332C17.1893 16.8332 17.9629 17.0963 18.4969 17.6303C19.0058 18.1605 19.2712 18.8823 19.2712 19.7877C19.2712 20.4058 19.0472 20.9945 18.5918 21.58C18.4467 21.7613 18.2378 21.977 17.565 22.6498C16.5022 23.579 15.8458 24.3349 15.4991 25.078C15.214 25.648 15.0687 26.2959 15.0687 26.9923V27.462C15.0687 27.6921 15.2552 27.8786 15.4853 27.8786H16.4929C16.723 27.8786 16.9096 27.6921 16.9096 27.462V26.9923C16.9096 26.4021 17.0549 25.8728 17.3559 25.3568C17.5813 24.9736 17.8708 24.673 18.3754 24.2315C19.3872 23.3161 19.9485 22.7708 20.1999 22.4509C20.8038 21.6538 21.1121 20.7409 21.1121 19.7423C21.1121 18.3641 20.6794 17.2584 19.8136 16.4407ZM15.5081 29.6968C15.2779 29.6968 15.0914 29.8834 15.0914 30.1135V31.0983C15.0914 31.3285 15.2779 31.515 15.5081 31.515H16.4929C16.723 31.515 16.9096 31.3285 16.9096 31.0983V30.1135C16.9096 29.8834 16.723 29.6968 16.4929 29.6968H15.5081Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2530" opacity="0.9" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#8F959E"/>
<path id="Rectangle 2531" opacity="0.6" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#646A73"/>
<path id="&#239;&#188;&#159;" d="M19.8136 16.4407C18.904 15.579 17.7103 15.1514 16.2258 15.1514C14.5581 15.1514 13.2511 15.686 12.3042 16.775C11.5021 17.7108 11.07 18.6077 11.0082 20.0217C11.0067 20.055 11.0055 20.1162 11.0044 20.187C11.001 20.4179 11.1884 20.6059 11.4195 20.6059H12.4045C12.6337 20.6059 12.8192 20.4209 12.823 20.1918C12.8242 20.1228 12.8255 20.0633 12.8271 20.0315C12.8755 19.0547 13.1303 18.5436 13.585 17.9752C14.1592 17.2098 15.0014 16.8332 16.1576 16.8332C17.1893 16.8332 17.9629 17.0963 18.4969 17.6303C19.0058 18.1605 19.2712 18.8823 19.2712 19.7877C19.2712 20.4058 19.0472 20.9945 18.5918 21.58C18.4467 21.7613 18.2378 21.977 17.565 22.6498C16.5022 23.579 15.8458 24.3349 15.4991 25.078C15.214 25.648 15.0687 26.2959 15.0687 26.9923V27.462C15.0687 27.6921 15.2552 27.8786 15.4853 27.8786H16.4929C16.723 27.8786 16.9096 27.6921 16.9096 27.462V26.9923C16.9096 26.4021 17.0549 25.8728 17.3559 25.3568C17.5813 24.9736 17.8708 24.673 18.3754 24.2315C19.3872 23.3161 19.9485 22.7708 20.1999 22.4509C20.8038 21.6538 21.1121 20.7409 21.1121 19.7423C21.1121 18.3641 20.6794 17.2584 19.8136 16.4407ZM15.5081 29.6968C15.2779 29.6968 15.0914 29.8834 15.0914 30.1135V31.0983C15.0914 31.3285 15.2779 31.515 15.5081 31.515H16.4929C16.723 31.515 16.9096 31.3285 16.9096 31.0983V30.1135C16.9096 29.8834 16.723 29.6968 16.4929 29.6968H15.5081Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99984C1 3.15889 2.49238 1.6665 4.33333 1.6665H20.3096C20.7517 1.6665 21.1756 1.8421 21.4882 2.15466L30.5118 11.1783C30.8244 11.4909 31 11.9148 31 12.3569V34.9998C31 36.8408 29.5076 38.3332 27.6667 38.3332H4.33333C2.49238 38.3332 1 36.8408 1 34.9998V4.99984Z" fill="#C6C6CD"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.27006C21 2.04733 21.2693 1.93579 21.4268 2.09328L30.5732 11.2397C30.7307 11.3972 30.6192 11.6665 30.3964 11.6665H24.3333C22.4924 11.6665 21 10.1741 21 8.33317V2.27006Z" fill="#A7A7B0"/>
<path id="&#229;&#189;&#162;&#231;&#138;&#182;" d="M7.66406 17.9165C7.66406 17.2261 8.22371 16.6665 8.91406 16.6665H17.6641C18.3544 16.6665 18.9141 17.2261 18.9141 17.9165V19.4009L20.8012 18.3393C21.6345 17.8706 22.6641 18.4728 22.6641 19.4288V25.1542C22.6641 26.1102 21.6345 26.7124 20.8012 26.2437L18.9141 25.1821V26.6665C18.9141 27.3569 18.3544 27.9165 17.6641 27.9165H8.91406C8.22371 27.9165 7.66406 27.3569 7.66406 26.6665V17.9165ZM11.4141 21.6665C12.1044 21.6665 12.6641 21.1069 12.6641 20.4165C12.6641 19.7261 12.1044 19.1665 11.4141 19.1665C10.7237 19.1665 10.1641 19.7261 10.1641 20.4165C10.1641 21.1069 10.7237 21.6665 11.4141 21.6665Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
<path id="&#229;&#189;&#162;&#231;&#138;&#182;" d="M7.66406 17.9165C7.66406 17.2261 8.22371 16.6665 8.91406 16.6665H17.6641C18.3544 16.6665 18.9141 17.2261 18.9141 17.9165V19.4009L20.8012 18.3393C21.6345 17.8706 22.6641 18.4728 22.6641 19.4288V25.1542C22.6641 26.1102 21.6345 26.7124 20.8012 26.2437L18.9141 25.1821V26.6665C18.9141 27.3569 18.3544 27.9165 17.6641 27.9165H8.91406C8.22371 27.9165 7.66406 27.3569 7.66406 26.6665V17.9165ZM11.4141 21.6665C12.1044 21.6665 12.6641 21.1069 12.6641 20.4165C12.6641 19.7261 12.1044 19.1665 11.4141 19.1665C10.7237 19.1665 10.1641 19.7261 10.1641 20.4165C10.1641 21.1069 10.7237 21.6665 11.4141 21.6665Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#C6C6CD"/>
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#A7A7B0"/>
<path id="icon_file_excel_nor" d="M10.2443 16.5151H12.064C12.1447 16.5151 12.2202 16.5548 12.266 16.6212L15.7637 21.6898L19.2798 16.6207C19.3256 16.5546 19.401 16.5151 19.4815 16.5151H21.3009C21.4365 16.5151 21.5464 16.625 21.5464 16.7606C21.5464 16.8117 21.5304 16.8615 21.5008 16.9031L16.9458 23.2925L21.8632 30.2185C21.9416 30.329 21.9156 30.4823 21.8051 30.5607C21.7636 30.5902 21.7139 30.606 21.663 30.606H19.8434C19.7628 30.606 19.6874 30.5665 19.6416 30.5002L15.7636 24.8954L11.9042 30.4998C11.8584 30.5663 11.7828 30.606 11.7021 30.606H9.88217C9.74661 30.606 9.63672 30.4962 9.63672 30.3606C9.63672 30.31 9.65237 30.2606 9.68153 30.2192L14.5623 23.2925L10.0438 16.9023C9.96558 16.7916 9.99186 16.6384 10.1025 16.5602C10.144 16.5309 10.1935 16.5151 10.2443 16.5151Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2526" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#32A645"/>
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#258832"/>
<path id="icon_file_excel_nor" d="M10.2443 16.5151H12.064C12.1447 16.5151 12.2202 16.5548 12.266 16.6212L15.7637 21.6898L19.2798 16.6207C19.3256 16.5546 19.401 16.5151 19.4815 16.5151H21.3009C21.4365 16.5151 21.5464 16.625 21.5464 16.7606C21.5464 16.8117 21.5304 16.8615 21.5008 16.9031L16.9458 23.2925L21.8632 30.2185C21.9416 30.329 21.9156 30.4823 21.8051 30.5607C21.7636 30.5902 21.7139 30.606 21.663 30.606H19.8434C19.7628 30.606 19.6874 30.5665 19.6416 30.5002L15.7636 24.8954L11.9042 30.4998C11.8584 30.5663 11.7828 30.606 11.7021 30.606H9.88217C9.74661 30.606 9.63672 30.4962 9.63672 30.3606C9.63672 30.31 9.65237 30.2606 9.68153 30.2192L14.5623 23.2925L10.0438 16.9023C9.96558 16.7916 9.99186 16.6384 10.1025 16.5602C10.144 16.5309 10.1935 16.5151 10.2443 16.5151Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#C6C6CD"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#A7A7B0"/>
<path id="&#229;&#144;&#136;&#229;&#185;&#182;&#229;&#189;&#162;&#231;&#138;&#182;" d="M21.714 15.8334C22.2399 15.8334 22.6663 16.2531 22.6663 16.7709V29.8959C22.6663 30.4136 22.2399 30.8334 21.714 30.8334H10.2854C9.7594 30.8334 9.33301 30.4136 9.33301 29.8959V16.7709C9.33301 16.2531 9.7594 15.8334 10.2854 15.8334H21.714ZM17.9044 26.1459H14.0949V29.4271H17.9044V26.1459ZM16.9521 27.0834V28.1682H15.0473V27.0834H16.9521ZM17.9484 19.5834H15.9997V21.4584H14.0949V23.3334H15.9997V25.2084H17.9484V23.3334H16.0436V21.4584H17.9484V19.5834ZM15.9997 17.7084H14.0949V19.5834H15.9997V17.7084Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Rectangle 2528" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#336DF4"/>
<path id="Rectangle 2529" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#0442D2"/>
<path id="&#229;&#144;&#136;&#229;&#185;&#182;&#229;&#189;&#162;&#231;&#138;&#182;" d="M21.714 15.8334C22.2399 15.8334 22.6663 16.2531 22.6663 16.7709V29.8959C22.6663 30.4136 22.2399 30.8334 21.714 30.8334H10.2854C9.7594 30.8334 9.33301 30.4136 9.33301 29.8959V16.7709C9.33301 16.2531 9.7594 15.8334 10.2854 15.8334H21.714ZM17.9044 26.1459H14.0949V29.4271H17.9044V26.1459ZM16.9521 27.0834V28.1682H15.0473V27.0834H16.9521ZM17.9484 19.5834H15.9997V21.4584H14.0949V23.3334H15.9997V25.2084H17.9484V23.3334H16.0436V21.4584H17.9484V19.5834ZM15.9997 17.7084H14.0949V19.5834H15.9997V17.7084Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { typeSafeAudioStaticToastVariants } from './variant';
export interface AudioStaticToastProps {
theme?: 'danger' | 'primary' | 'background';
className?: string;
color?: 'primary' | 'danger';
}
export const AudioStaticToast: React.FC<
PropsWithChildren<AudioStaticToastProps>
> = ({ children, theme = 'primary', color = 'primary', className }) => {
const cvaClassNames = typeSafeAudioStaticToastVariants({ theme, color });
return <div className={classNames(cvaClassNames, className)}>{children}</div>;
};

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cva, type VariantProps } from 'class-variance-authority';
const audioStaticToastVariants = cva(['px-24px', 'py-10px', 'rounded-[99px]'], {
variants: {
theme: {
primary: ['bg-[#F2F3F7]'],
danger: ['bg-[#FFEFF1]'],
background: ['coz-bg-image-bots'],
},
color: {
primary: ['coz-fg-primary'],
danger: ['coz-fg-hglt-red'],
},
},
});
export type AudioStaticToastVariantsProps = Required<
VariantProps<typeof audioStaticToastVariants>
>;
export const typeSafeAudioStaticToastVariants: (
props: AudioStaticToastVariantsProps,
) => string = audioStaticToastVariants;

View File

@@ -0,0 +1,19 @@
.container {
overflow: hidden;
align-items: center;
justify-content: center;
width: 100%;
height: 24px;
.bar {
width: 4px;
min-height: 6px;
max-height: 24px;
background-color: #AAA;
border-radius: 8px;
transition: height 0.1s ease-in-out; /* 设置过渡效果 */
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { Space } from '@coze-arch/coze-design';
import { getBarBgColor, getBarHeights } from './utils';
import { type AudioWaveProps } from './type';
import styles from './index.module.less';
const waveBarNumberMap = {
large: 41,
medium: 29,
small: 4,
};
export const AudioWave = ({
size = 'medium',
volumeNumber = 0,
type = 'default',
wrapperClassName,
waveClassName,
}: AudioWaveProps) => {
const volumeRealNumber = Math.max(Math.min(volumeNumber, 100), 0);
const waveBarNumber = waveBarNumberMap[size] || 29;
const waveBarHeights = getBarHeights(waveBarNumber, volumeRealNumber);
return (
<Space
spacing={3}
align="center"
className={classNames(styles.container, wrapperClassName)}
>
{waveBarHeights.map((height, index) => (
<div
className={classNames(
styles[`audio-wave-${index}`],
styles[type],
styles.bar,
styles[size],
waveClassName,
)}
style={{
backgroundColor: getBarBgColor(index, waveBarNumber, type),
height,
}}
key={`${type}_${index}`}
/>
))}
</Space>
);
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface AudioWaveProps {
size: 'small' | 'medium' | 'large';
type: 'default' | 'primary' | 'warning';
/** 0 ~ 100 */
volumeNumber: number;
wrapperClassName?: string;
waveClassName?: string;
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type AudioWaveProps } from './type';
export const getBarHeights = (
waveBarNumber: number,
volumeRealNumber: number,
) => {
if (volumeRealNumber <= 0) {
return new Array(waveBarNumber).fill(8);
}
const waveBarHeights = new Array(waveBarNumber)
.fill(0)
.map((_item, index) =>
getBarHeight3(index, waveBarNumber, volumeRealNumber),
);
const minHeight = Math.min(...waveBarHeights);
const maxHeight = Math.max(...waveBarHeights);
const heightSpan = maxHeight - minHeight;
return waveBarHeights.map(
item =>
8 +
((item - minHeight) / (maxHeight - minHeight)) * Math.min(12, heightSpan),
);
};
export const getBarHeight3 = (
index: number,
maxNumber: number,
volumeNumber: number,
) => {
const percent = index / maxNumber;
const maxHeight = 24;
let baseHeight = 2;
let randomMin = -2;
let randomMax = 2;
if (percent < 1 / 6) {
baseHeight = 1 + (4 - 1) * percent * 6;
randomMin = 0.1 + (-0.8 - 0.1) * percent * 6;
randomMax = 0.3 + (0.6 - 0.3) * percent * 6;
} else if (percent < 2 / 6) {
baseHeight = 4 + (2 - 4) * (percent - 1 / 6) * 6;
randomMin = -0.8 + (-0.0 + 0.8) * (percent - 1 / 6) * 6;
randomMax = 0.6 + (0.6 - 0.6) * (percent - 1 / 6) * 6;
} else if (percent < 3 / 6) {
baseHeight = 2 + (8 - 2) * (percent - 2 / 6) * 6;
randomMin = 0.0 + (Number(-1.6) - 0.0) * (percent - 2 / 6) * 6;
randomMax = 0.6 + (1.2 - 0.6) * (percent - 2 / 6) * 6;
} else if (percent < 4 / 6) {
baseHeight = 8 + (2 - 8) * (percent - 3 / 6) * 6;
randomMin = -1.6 + (0.0 + 1.6) * (percent - 3 / 6) * 6;
randomMax = 1.2 + (0.6 - 1.2) * (percent - 3 / 6) * 6;
} else if (percent < 5 / 6) {
baseHeight = 2 + (4 - 2) * (percent - 4 / 6) * 6;
randomMin = 0.0 + (Number(-0.8) - 0.0) * (percent - 4 / 6) * 6;
randomMax = 0.6 + (0.6 - 0.6) * (percent - 4 / 6) * 6;
} else if (percent < 1) {
baseHeight = 4 + (1 - 4) * (percent - 5 / 6) * 6;
randomMin = -0.8 + (0.1 + 0.8) * (percent - 5 / 6) * 6;
randomMax = 0.1 + (0.3 - 0.6) * (percent - 5 / 6) * 6;
}
const height =
baseHeight +
volumeNumber *
(Math.random() * (randomMax - randomMin) + randomMin) *
(maxHeight - baseHeight);
return height;
};
export const getBarBgColor = (
index: number,
maxNumber: number,
type: AudioWaveProps['type'],
) => {
let bgColor = '#FFF';
switch (type) {
case 'primary':
{
/*
* implement : fill: linear-gradient(90deg, rgba(83, 71, 255, 0.20) 0%, #5347FF 20%, #B125F1 80%, rgba(177, 37, 241, 0.20) 100%);
*/
let opacity = 0;
let rColor = 0;
let gColor = 0;
let bColor = 0;
const percent = index / maxNumber;
if (percent < 0.2) {
opacity = 0.2 + ((1 - 0.2) * percent) / 0.2;
rColor = 83;
gColor = 71;
bColor = 255;
} else if (percent < 0.8) {
opacity = 1;
rColor = Math.round(83 + ((177 - 83) * (percent - 0.2)) / 0.6);
gColor = Math.round(71 + ((37 - 71) * (percent - 0.2)) / 0.6);
bColor = Math.round(255 + ((241 - 255) * (percent - 0.2)) / 0.6);
} else {
opacity = 1 - ((1 - 0.2) * (percent - 0.8)) / 0.2;
rColor = 177;
gColor = 37;
bColor = 241;
}
bgColor = `rgba(${rColor}, ${gColor}, ${bColor}, ${opacity.toFixed(
2,
)})`;
}
break;
case 'warning':
{
bgColor = '#FF0030';
}
break;
default:
{
/*
* implement : fill: linear-gradient(90deg, rgba(255, 255, 255, 0.20) 0%, #FFF 20%, rgba(255, 255, 255, 0.90) 80%, rgba(255, 255, 255, 0.20) 100%);
*/
let opacity = 0;
const percent = index / maxNumber;
if (percent < 0.2) {
opacity = 0.2 + ((1 - 0.2) * percent) / 0.2;
} else if (percent < 0.8) {
opacity = 1 - ((1 - 0.9) * (percent - 0.2)) / 0.6;
} else {
opacity = 0.9 - ((0.9 - 0.2) * (percent - 0.8)) / 0.2;
}
bgColor = `rgba(255, 255, 255, ${opacity.toFixed(2)})`;
}
break;
}
return bgColor;
};

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forwardRef, useEffect, useRef, useState } from 'react';
import { useThrottleFn } from 'ahooks';
import { type AudioRecordProps, Layout } from '@coze-common/chat-uikit-shared';
import { type AudioWaveProps } from './audio-wave/type';
import { AudioWave } from './audio-wave';
export const AudioRecord = forwardRef<
HTMLDivElement,
AudioRecordProps & { layout: Layout }
>(({ isRecording, getVolume, isPointerMoveOut, layout, text }, ref) => {
const [volumeNumber, setVolumeNumber] = useState(0);
const animationIdRef = useRef<number | null>(null);
const { run, flush } = useThrottleFn(
() => {
setVolumeNumber(getVolume?.() ?? 0);
animationIdRef.current = requestAnimationFrame(run);
},
{ wait: 100 },
);
const getAudioWaveTheme = (): AudioWaveProps['type'] => {
if (layout === Layout.MOBILE) {
return 'default';
}
if (isPointerMoveOut) {
return 'warning';
}
return 'primary';
};
useEffect(() => {
if (!isRecording) {
return;
}
run();
return () => {
flush();
if (typeof animationIdRef.current !== 'number') {
return;
}
cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null;
};
}, [isRecording]);
return (
<div ref={ref} className="w-full h-32px relative">
<div className="w-full h-full flex items-center justify-center pointer-events-none">
{isRecording ? (
<AudioWave
size="medium"
type={getAudioWaveTheme()}
volumeNumber={volumeNumber}
/>
) : (
<div className="coz-fg-primary text-lg font-medium leading-[20px]">
{text}
</div>
)}
</div>
</div>
);
});
export { AudioWave };

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode, type FC } from 'react';
import classNames from 'classnames';
import { IconCozChatPlus } from '@coze-arch/coze-design/icons';
import { Layout } from '@coze-common/chat-uikit-shared';
import { UIKitTooltip } from '../../../../common/tooltips';
import { OutlinedIconButton } from '../../../../common';
interface IProps {
isDisabled?: boolean;
tooltipContent?: ReactNode;
onClick: () => void;
layout: Layout;
className: string;
showBackground: boolean;
}
const ClearContextButton: FC<IProps> = props => {
const {
isDisabled,
tooltipContent,
onClick,
layout,
className,
showBackground,
} = props;
return (
<UIKitTooltip
content={tooltipContent}
hideToolTip={layout === Layout.MOBILE}
>
<OutlinedIconButton
data-testid="chat-input-clear-context-button"
showBackground={showBackground}
disabled={isDisabled}
icon={<IconCozChatPlus className="text-18px" />}
size="default"
onClick={onClick}
className={classNames('mr-12px', '!rounded-full', className)}
/>
</UIKitTooltip>
);
};
export default ClearContextButton;

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode, type FC } from 'react';
import classNames from 'classnames';
import { IconCozBroom } from '@coze-arch/coze-design/icons';
import { Layout } from '@coze-common/chat-uikit-shared';
import { UIKitTooltip } from '../../../../common/tooltips';
import { OutlinedIconButton } from '../../../../common';
interface IProps {
isDisabled?: boolean;
tooltipContent?: ReactNode;
onClick: () => void;
layout: Layout;
className: string;
showBackground: boolean;
}
const ClearHistoryButton: FC<IProps> = props => {
const {
isDisabled,
tooltipContent,
onClick,
layout,
className,
showBackground,
} = props;
return (
<UIKitTooltip
content={tooltipContent}
hideToolTip={layout === Layout.MOBILE}
>
<OutlinedIconButton
data-testid="bot-edit-debug-chat-clear-button"
showBackground={showBackground}
disabled={isDisabled}
icon={<IconCozBroom className="text-18px" />}
size="default"
onClick={onClick}
className={classNames('mr-12px', '!rounded-full', className)}
/>
</UIKitTooltip>
);
};
export default ClearHistoryButton;

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode, type FC } from 'react';
import classNames from 'classnames';
import { IconCozPlusCircle } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { Layout } from '@coze-common/chat-uikit-shared';
import { UIKitTooltip } from '../../../../common/tooltips';
interface IProps {
isDisabled?: boolean;
tooltipContent?: ReactNode;
layout: Layout;
}
const MoreButton: FC<IProps> = props => {
const { isDisabled, tooltipContent, layout } = props;
return (
<UIKitTooltip
// 为了点调起选择文件的事件时收起 tooltip
disableFocusListener
content={tooltipContent}
hideToolTip={layout === Layout.MOBILE}
>
<IconButton
className="!rounded-full"
data-testid="chat-area.chat-upload-button"
color="secondary"
disabled={isDisabled}
icon={
<IconCozPlusCircle
className={classNames(
isDisabled ? 'coz-fg-dim' : 'coz-fg-primary',
'text-18px',
)}
/>
}
/>
</UIKitTooltip>
);
};
export default MoreButton;

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import classNames from 'classnames';
import { IconCozSendFill } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { Layout, type SendButtonProps } from '@coze-common/chat-uikit-shared';
import { UIKitTooltip } from '../../../../common/tooltips';
const SendButton: FC<SendButtonProps> = props => {
const { isDisabled, tooltipContent, onClick, layout } = props;
return (
<UIKitTooltip
content={tooltipContent}
hideToolTip={layout === Layout.MOBILE}
>
<IconButton
className={classNames('!rounded-full', !isDisabled && '!coz-fg-hglt')}
disabled={isDisabled}
data-testid="bot-home-chart-send-button"
size="default"
color="secondary"
icon={<IconCozSendFill className="text-18px" />}
onClick={onClick}
/>
</UIKitTooltip>
);
};
export default SendButton;

View File

@@ -0,0 +1,184 @@
.input-wrap {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
@keyframes move-left-fade-out {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(-100%);
opacity: 0;
}
}
@keyframes move-right-fade-in {
0% {
transform: translateX(-100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@fade-easing-time: 300ms;
@fade-easing-function: cubic-bezier(0.65, 0, 0.35, 1);
.animate-left {
animation: move-left-fade-out forwards @fade-easing-time @fade-easing-function;
}
.animate-left-revert {
animation: move-right-fade-in forwards @fade-easing-time @fade-easing-function;
}
.input-container {
display: flex;
flex-direction: column;
}
.input-tooltip-anchor {
position: absolute;
top: -10%;
}
.left-actions-container {
align-self: flex-end;
width: fit-content;
}
.textarea-with-top-rows {
display: flex;
flex-direction: column;
border-style: solid;
border-width: 1px;
border-radius: 24px;
transition: width @fade-easing-time;
}
.background-theme {
textarea {
// 背景图模式覆盖样式
/* stylelint-disable-next-line declaration-no-important */
background: transparent !important;
}
}
.textarea-with-actions-container {
position: relative;
flex: 1;
align-items: center;
min-height: 48px;
}
.input-focus {
border-style: solid;
border-width: 1px;
}
/* stylelint-disable-next-line selector-class-pattern */
.textarea-with-actions-container__col {
flex-direction: column;
}
/* stylelint-disable-next-line selector-class-pattern */
.textarea-with-actions-container__row {
display: flex;
flex-direction: row;
}
.textarea-actions-container {
display: flex;
flex-direction: row-reverse;
align-items: center;
align-self: flex-end;
}
.textarea-actions-container-transition {
transition: opacity @fade-easing-time @fade-easing-function
}
.textarea-actions-left {
display: flex;
gap: 4px;
align-items: center;
margin-right: 4px;
}
.textarea-actions-right {
margin-left: 4px;
}
.textarea {
display: flex;
width: 100%;
height: 20px;
min-height: 20px;
margin: 0;
font-family: inherit;
font-weight: 500;
line-height: 20px;
overflow-wrap: break-word;
white-space: pre-wrap;
border: none;
border-radius: 0;
outline: none;
&::-webkit-scrollbar {
width: 6px;
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #e6e6e7;
border-radius: 4px;
}
}
.textarea::placeholder {
font-weight: 500;
}
.textarea:disabled {
background: #fff;
}
.bottom-tips {
width: 100%;
padding: 8px 24px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
text-align: center;
}
.danger-mask-transition {
transition: opacity 500ms cubic-bezier(0.33, 1, 0.68, 1);
}
.mobile-audio-bg {
/* stylelint-disable-next-line declaration-no-important */
background: linear-gradient(117deg, #5448FF 4.74%, #B026F1 131.42%) !important;
}
.mobile-audio-bg-danger {
@apply !coz-mg-hglt-plus-red;
}

View File

@@ -0,0 +1,527 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
forwardRef,
useImperativeHandle,
useState,
type Dispatch,
type ForwardRefExoticComponent,
type PropsWithChildren,
type PropsWithoutRef,
type RefAttributes,
type RefObject,
type SetStateAction,
useRef,
useEffect,
} from 'react';
import Textarea, { type TextAreaRef } from 'rc-textarea';
import { merge } from 'lodash-es';
import classNames from 'classnames';
import { useControllableValue, useUpdateEffect } from 'ahooks';
import {
IconCozKeyboard,
IconCozMicrophone,
} from '@coze-arch/coze-design/icons';
import { Divider, IconButton } from '@coze-arch/coze-design';
import {
type IChatInputProps,
type InputMode,
Layout,
} from '@coze-common/chat-uikit-shared';
import { AudioRecord } from '../audio-record';
import { UIKitTooltip } from '../../common';
import { useAudioRecordInteraction } from '../../../hooks/use-audio-record-interaction';
import { ChatUpload } from '../../../components/chat/chat-upload';
import { useTextSend } from './use-text-area';
import BuiltinSendButton from './components/send-button';
import MoreButton from './components/more-button';
import ClearHistoryButton from './components/clear-history-button';
import ClearContextButton from './components/clear-context-button';
import styles from './index.module.less';
const DEFAULT_HEIGHT = 24;
export interface UiKitChatInputButtonConfig {
isSendButtonVisible: boolean;
isClearHistoryButtonVisible: boolean;
isMoreButtonVisible: boolean;
isClearContextButtonVisible: boolean;
}
export type ChatInputTooltipComponent = ForwardRefExoticComponent<
PropsWithoutRef<PropsWithChildren> &
RefAttributes<{
close: () => void;
}>
>;
export interface InputRefObject {
input: RefObject<TextAreaRef>;
setValue: Dispatch<SetStateAction<string>>;
sendMessage: IChatInputProps['onSendMessage'];
}
export const ChatInput = forwardRef<InputRefObject, IChatInputProps>(
// eslint-disable-next-line complexity, @coze-arch/max-line-per-function, max-lines-per-function
(props, ref) => {
const {
onBeforeSubmit,
onFocus,
onBlur,
isReadonly,
leftActions,
rightActions,
addonTop,
addonLeft,
aboveOutside,
buildInButtonConfig,
buildInButtonStatus,
copywritingConfig,
onSendMessage,
onClearHistory,
onClearContext,
onUpload,
onInputClick,
hasOtherContentToSend,
layout,
isFileCountExceedsLimit,
inputTooltip: InputTooltip,
showBackground,
limitFileCount,
onPaste,
CustomSendButton,
isInputReadonly,
inputNativeCallbacks,
audioRecordEvents = {},
audioRecordState = {},
audioRecordOptions,
className: _className,
wrapperClassName,
...inputModeProps
} = props;
const {
isSendButtonVisible = true,
isClearHistoryButtonVisible = true,
isMoreButtonVisible = true,
isClearContextButtonVisible = false,
} = buildInButtonConfig ?? {};
const {
isClearHistoryButtonDisabled,
isMoreButtonDisabled,
isSendButtonDisabled,
isClearContextButtonDisabled,
} = buildInButtonStatus ?? {};
const { tooltip, inputPlaceholder, uploadConfig, bottomTips } =
copywritingConfig ?? {};
const {
sendButtonTooltipContent,
clearHistoryButtonTooltipContent,
clearContextButtonTooltipContent,
moreButtonTooltipContent,
audioButtonTooltipContent,
keyboardButtonTooltipContent,
} = tooltip ?? {};
const [isMultiLines, setIsMultiLines] = useState(false);
const [breakLength, setBreakLength] = useState(0);
const [isFocus, setIsFocus] = useState(false);
const [mode, setMode] = useControllableValue<InputMode>(inputModeProps, {
defaultValue: 'input',
valuePropName: 'inputMode',
trigger: 'onInputModeChange',
});
const audioButtonWrapperRef = useRef<HTMLDivElement>(null);
useAudioRecordInteraction({
target: audioButtonWrapperRef,
events: audioRecordEvents,
options: merge({}, audioRecordOptions, {
enabled: mode === 'audio' && audioRecordOptions?.enabled,
}),
});
const inputSizeRef = useRef<HTMLDivElement>(null);
const [inputWidth, setInputWidth] = useState<'100%' | number>('100%');
useEffect(() => {
const target = inputSizeRef.current;
if (!target) {
return;
}
const observer = new ResizeObserver(() => {
setInputWidth(target.clientWidth);
});
observer.observe(target);
return () => {
observer.disconnect();
};
}, []);
const isInputMode = mode === 'input';
const isAudioMode = mode === 'audio';
const handleClickContextButtonClick = () => {
onClearContext?.();
};
/**
* 处理清除历史记录按钮点击事件
*/
const handleClearHistoryButtonClick = () => {
onClearHistory?.();
};
/**
* 处理用户发送消息
* @param text 用户发送消息的文本
*/
const handleSendMessage = (text: string) => {
onSendMessage?.({
text,
mentionList: [],
});
setIsMultiLines(false);
setBreakLength(0);
};
const {
onChange,
setIsComposing,
onKeyDown,
inputText,
setInputText,
submit: handleSendButtonClick,
rcTextareaRef,
updateSelectPos,
onKeyUp,
} = useTextSend({
onSubmit: handleSendMessage,
onBeforeSubmit,
isDisabled: isSendButtonDisabled,
allowEmpty: hasOtherContentToSend,
inputNativeCallbacks,
});
const buttonClass = showBackground ? '!coz-fg-images-white' : '';
const handleResize = ({ height }: { height: number }) => {
if (
!isMultiLines &&
height > DEFAULT_HEIGHT &&
// 通过 inputText 长度判断,排除 placeholder 导致的 resize 不处理
inputText?.trim()?.length !== 0
) {
setIsMultiLines(true);
setBreakLength(inputText.length);
}
};
const handleOnChange = (evt: { target: { value: string } }) => {
onChange(evt);
if (isMultiLines && evt.target.value.length < breakLength) {
setIsMultiLines(false);
}
};
/** 计算出复合条件的readonly值 */
const finalClearHistoryButtonDisable =
isClearHistoryButtonDisabled || isReadonly;
const finalSendButtonDisable =
(Boolean(!inputText?.trim()) && !hasOtherContentToSend) ||
isSendButtonDisabled ||
isReadonly;
const finalMoreButtonDisable = isMoreButtonDisabled || isReadonly;
const finalClearContextButtonDisable =
isClearContextButtonDisabled || isReadonly;
const getFinalSendButtonVisible = () => {
const visibleCondition = isSendButtonVisible && isInputMode;
if (audioRecordOptions?.enabled) {
return visibleCondition && inputText;
}
return visibleCondition;
};
useUpdateEffect(() => {
if (mode !== 'input') {
return;
}
rcTextareaRef.current?.focus();
}, [mode]);
useImperativeHandle(ref, () => ({
input: rcTextareaRef,
setValue: setInputText,
sendMessage: onSendMessage,
}));
const SendButton = CustomSendButton ?? BuiltinSendButton;
return (
<div className={styles['input-container']}>
<div
className={classNames(
styles['input-wrap'],
'py-0',
layout === Layout.MOBILE ? 'px-[16px]' : 'px-[24px]',
'input-wraper-for-reset',
)}
>
{aboveOutside}
<div
className={classNames(
styles['left-actions-container'],
['mb-8px'],
typeof audioRecordState.isRecording === 'boolean' &&
audioRecordState.isRecording
? [styles['animate-left'], '!w-0']
: styles['animate-left-revert'],
)}
>
{leftActions}
{isClearContextButtonVisible ? (
<ClearContextButton
showBackground={Boolean(showBackground)}
tooltipContent={clearContextButtonTooltipContent}
isDisabled={finalClearContextButtonDisable}
onClick={handleClickContextButtonClick}
data-testid="bot-edit-debug-chat-clear-button"
layout={layout}
className={buttonClass}
></ClearContextButton>
) : null}
{isClearHistoryButtonVisible ? (
<ClearHistoryButton
showBackground={Boolean(showBackground)}
tooltipContent={clearHistoryButtonTooltipContent}
isDisabled={finalClearHistoryButtonDisable}
onClick={handleClearHistoryButtonClick}
data-testid="bot-edit-debug-chat-clear-button"
layout={layout}
className={buttonClass}
/>
) : null}
</div>
<div
ref={inputSizeRef}
className="flex-[1] flex justify-end overflow-hidden"
>
<div
style={{ width: inputWidth }}
className={classNames(
styles['textarea-with-top-rows'],
['coz-bg-max', 'coz-stroke-plus', 'relative'],
isFocus && styles['input-focus'],
(isFocus || audioRecordState.isRecording) && [
'!coz-stroke-hglt',
],
audioRecordState.isRecording &&
audioRecordState.isPointerMoveOut &&
'!coz-stroke-hglt-red',
audioRecordState.isRecording
? 'overflow-visible'
: 'overflow-hidden',
mode === 'audio' && 'cursor-pointer',
(mode === 'audio' || audioRecordState.isRecording) &&
'hover:coz-stroke-hglt',
showBackground && styles['background-theme'],
{ '!coz-bg-image-bots': showBackground },
layout === Layout.MOBILE &&
audioRecordState.isRecording &&
(audioRecordState.isPointerMoveOut
? styles['mobile-audio-bg-danger']
: styles['mobile-audio-bg']),
wrapperClassName,
)}
>
{addonTop}
<div
className={classNames(
styles['textarea-with-actions-container'],
styles['coz-textarea-with-actions-container-padding'],
'py-8px',
'pr-8px',
'pl-20px',
{
[styles['textarea-with-actions-container__row']]:
!isMultiLines,
[styles['textarea-with-actions-container__col']]:
isMultiLines,
},
)}
>
{InputTooltip ? (
<InputTooltip>
<i className={styles['input-tooltip-anchor']} />
</InputTooltip>
) : null}
{addonLeft}
{isInputMode ? (
<Textarea
data-testid="bot.ide.chat_area.chat_input.textarea"
disabled={isInputReadonly || isReadonly}
className={classNames(
styles.textarea,
[
'coz-fg-primary',
'coz-bg-max',
'disabled:coz-bg-max',
'placeholder:coz-fg-dim',
],
{
[styles['textarea--with-margin']]: isMultiLines,
},
)}
autoSize={{
minRows: 1,
maxRows: 5,
}}
classNames={{
textarea: classNames(
styles.textarea,
layout === Layout.MOBILE
? 'text-[16px]'
: 'text-[14px]',
),
}}
ref={rcTextareaRef}
placeholder={inputPlaceholder}
onChange={handleOnChange}
value={inputText}
onCompositionStart={evt => setIsComposing(evt, true)}
onCompositionEnd={evt => setIsComposing(evt, false)}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onResize={handleResize}
onSelect={updateSelectPos}
onClick={onInputClick}
onFocus={e => {
onFocus?.(e);
setIsFocus(true);
}}
onBlur={e => {
onBlur?.(e);
setIsFocus(false);
}}
onPaste={onPaste}
/>
) : null}
{isAudioMode ? (
<AudioRecord
ref={audioButtonWrapperRef}
{...audioRecordState}
layout={layout}
/>
) : null}
<div
className={classNames(
audioRecordState.isRecording
? 'opacity-0 w-0'
: 'opacity-100',
styles['textarea-actions-container'],
styles['textarea-actions-container-transition'],
)}
>
<div className={styles['textarea-actions-right']}>
{getFinalSendButtonVisible() ? (
<SendButton
tooltipContent={sendButtonTooltipContent}
isDisabled={finalSendButtonDisable}
onClick={handleSendButtonClick}
layout={layout}
/>
) : null}
{!inputText &&
!isAudioMode &&
audioRecordOptions?.enabled ? (
<UIKitTooltip
content={audioButtonTooltipContent}
hideToolTip={layout === Layout.MOBILE}
>
<IconButton
className="!rounded-full"
color="secondary"
icon={<IconCozMicrophone className="text-18px" />}
onClick={() => {
setMode('audio');
}}
/>
</UIKitTooltip>
) : null}
{isAudioMode ? (
<UIKitTooltip
content={keyboardButtonTooltipContent}
hideToolTip={layout === Layout.MOBILE}
>
<IconButton
className="!rounded-full"
color="secondary"
icon={<IconCozKeyboard className="text-18px" />}
onClick={() => {
setMode('input');
}}
/>
</UIKitTooltip>
) : null}
</div>
{rightActions || isMoreButtonVisible ? (
<>
<Divider layout="vertical" style={{ height: '14px' }} />
<div className={styles['textarea-actions-left']}>
{rightActions}
{isMoreButtonVisible ? (
<ChatUpload
onUpload={(uploadType, file) =>
onUpload?.(uploadType, { file, mentionList: [] })
}
isFileCountExceedsLimit={isFileCountExceedsLimit}
copywritingConfig={uploadConfig}
isDisabled={finalMoreButtonDisable}
limitFileCount={limitFileCount}
>
<MoreButton
tooltipContent={moreButtonTooltipContent}
isDisabled={finalMoreButtonDisable}
layout={layout}
/>
</ChatUpload>
) : null}
</div>
</>
) : null}
</div>
</div>
</div>
</div>
</div>
{bottomTips ? (
<div
className={classNames(
styles['bottom-tips'],
'coz-fg-dim',
showBackground && '!coz-fg-images-secondary',
)}
>
<span>{bottomTips}</span>
</div>
) : null}
</div>
);
},
);
ChatInput.displayName = 'UiKitChatInput';

View File

@@ -0,0 +1,224 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useState,
type KeyboardEvent,
useRef,
useEffect,
type KeyboardEventHandler,
type CompositionEvent,
} from 'react';
import { type TextAreaRef } from 'rc-textarea';
import {
type InputNativeCallbacks,
type InputController,
type InputState,
} from '@coze-common/chat-uikit-shared';
import { useImperativeLayoutEffect } from '@coze-common/chat-hooks';
type KeyboardGeneralEvent = KeyboardEvent<HTMLElement>;
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- x.x
export const useTextSend = ({
onSubmit,
defaultValue = '',
allowEmpty = false,
onBeforeSubmit,
isDisabled = false,
inputNativeCallbacks = {},
}: {
onSubmit: (text: string) => void;
defaultValue?: string;
/**
* 是否允许空字符串提交
* @default false
*/
allowEmpty?: boolean;
onBeforeSubmit?: () => boolean;
isDisabled?: boolean;
inputNativeCallbacks?: InputNativeCallbacks;
}) => {
const [inputText, setInputText] = useState(defaultValue);
const composingRef = useRef(false);
const rcTextareaRef = useRef<TextAreaRef>(null);
const selectionRef = useRef<{ start: number; end: number }>({
start: 0,
end: 0,
});
const getTextarea = () => rcTextareaRef.current?.resizableTextArea.textArea;
const waitAndUpdateSelectPos = useImperativeLayoutEffect(() =>
updateSelectPos(),
);
const readState = (): InputState => {
const state: InputState = {
inputText,
isComposing: composingRef.current,
isDisabled,
selection: selectionRef.current,
hasSelection: selectionRef.current.start !== selectionRef.current.end,
};
return state;
};
const readStateRef = useRef<() => InputState>();
readStateRef.current = readState;
useEffect(() => {
if (!inputNativeCallbacks.getController) {
return;
}
const controller: InputController = {
requireSetMousePosition,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- .
readState: () => readStateRef.current!(),
setInputText: (updater: string | ((pre: string) => string)) => {
setInputText(updater);
waitAndUpdateSelectPos();
},
focus: () => {
getTextarea()?.focus();
},
};
inputNativeCallbacks.getController(controller);
}, [inputNativeCallbacks.getController]);
const requireSetMousePosition = useImperativeLayoutEffect((pos: number) => {
const textarea = getTextarea();
if (!textarea) {
return;
}
textarea.focus();
textarea.setSelectionRange(pos, pos);
});
const updateSelectPos = () => {
const textarea = getTextarea();
if (!textarea) {
return;
}
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
selectionRef.current = { start, end };
};
const submit = () => {
if (!allowEmpty && !inputText.trim()) {
return;
}
if (isDisabled) {
return;
}
if (onBeforeSubmit && !onBeforeSubmit()) {
return;
}
onSubmit(inputText);
setInputText('');
};
const onKeydownToSubmit = (evt: KeyboardGeneralEvent) => {
if (evt.code !== 'Enter') {
return;
}
if (composingRef.current || isPressEnterToChangeLine(evt)) {
return;
}
evt.preventDefault();
submit();
};
const onKeyUp: KeyboardEventHandler<HTMLTextAreaElement> = e => {
updateSelectPos();
inputNativeCallbacks.onAfterProcessKeyUp?.(e);
};
const onKeyDown = (evt: KeyboardEvent<HTMLTextAreaElement>) => {
updateSelectPos();
const res = inputNativeCallbacks.onBeforeProcessKeyDown?.(evt);
if (res?.exit) {
return;
}
if ((evt.metaKey || evt.altKey || evt.ctrlKey) && evt.code === 'Enter') {
handleNewLine();
return;
}
onKeydownToSubmit(evt);
};
const handleNewLine = () => {
const textarea = getTextarea();
if (!textarea) {
return;
}
// 计算光标的当前位置
const cursorPosition = textarea.selectionStart;
// 在光标位置插入新行
const newValue = `${inputText.substring(
0,
cursorPosition,
)}\n${inputText.substring(cursorPosition)}`;
setInputText(newValue);
setTimeout(() => {
textarea.selectionStart = cursorPosition + 1;
textarea.selectionEnd = cursorPosition + 1;
}, 0);
};
const onChange = (evt: { target: { value: string } }) => {
updateSelectPos();
const val = evt.target.value;
setInputText(val);
Promise.resolve().then(() => {
inputNativeCallbacks?.onAfterOnChange?.();
});
};
return {
onChange,
setIsComposing: (
_: CompositionEvent<HTMLTextAreaElement>,
composing: boolean,
) => {
composingRef.current = composing;
},
submit,
onKeyDown,
inputText,
setInputText,
rcTextareaRef,
updateSelectPos,
onKeyUp,
};
};
const isPressEnterToChangeLine = (evt: KeyboardGeneralEvent) => {
if (evt.code !== 'Enter') {
return false;
}
return evt.shiftKey || evt.altKey || evt.metaKey;
};

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import {
FILE_TYPE_CONFIG,
FileTypeEnum,
} from '@coze-common/chat-core/shared/const';
import { Toast, Upload } from '@coze-arch/coze-design';
import {
type IChatUploadCopywritingConfig,
DEFAULT_MAX_FILE_SIZE,
UploadType,
} from '@coze-common/chat-uikit-shared';
interface IChatUploadProps {
/**
* 上传事件回调
* @param uploadType 上传类型 [IMAGE=0 FILE=1]
* @param file 文件
* @returns void
*/
onUpload: (uploadType: UploadType, file: File) => void;
/**
* 文案信息配置
*/
copywritingConfig?: IChatUploadCopywritingConfig;
/**
* 文件最大尺寸单位byte
*/
maxFileSize?: number;
isDisabled?: boolean;
children: JSX.Element;
limitFileCount?: number;
isFileCountExceedsLimit: (fileCount: number) => boolean;
}
const findFileTypeConfig = (file: File) =>
FILE_TYPE_CONFIG.find(
cnf => cnf.judge?.(file) || cnf.accept.some(ext => file.name.endsWith(ext)),
);
export const ChatUpload: FC<IChatUploadProps> = props => {
const {
copywritingConfig = {},
maxFileSize = DEFAULT_MAX_FILE_SIZE,
children,
onUpload,
isDisabled,
isFileCountExceedsLimit,
limitFileCount = 1,
} = props;
/**
* 处理上传
* @param fileList 文件List
* @returns void
*/
const handleUpload = (fileList: File[]) => {
const { fileSizeReachLimitToast, fileExceedsLimitToast, fileEmptyToast } =
copywritingConfig;
if (isFileCountExceedsLimit(fileList.length)) {
Toast.warning({
showClose: false,
content: fileExceedsLimitToast,
});
return;
}
if (!fileList.length) {
return;
}
// 是否存在超出大小的文件
const hasOverflowLimitFileSize = fileList.some(
file => file.size > maxFileSize,
);
const hasEmptyFile = fileList.some(file => file.size <= 0);
// 文件大小超过预期大小的错误处理
if (hasOverflowLimitFileSize) {
Toast.warning({
showClose: false,
content: fileSizeReachLimitToast,
});
}
if (hasEmptyFile) {
Toast.warning({
showClose: false,
content: fileEmptyToast,
});
}
const verifiedFileTypeConfigList = fileList
.filter(file => file.size <= maxFileSize && file.size > 0)
.map(file => ({
file,
fileTypeConfig: findFileTypeConfig(file),
}));
for (const fileConfig of verifiedFileTypeConfigList) {
if (fileConfig.fileTypeConfig?.fileType === FileTypeEnum.IMAGE) {
onUpload?.(UploadType.IMAGE, fileConfig.file);
} else {
onUpload?.(UploadType.FILE, fileConfig.file);
}
}
};
return (
<Upload
limit={limitFileCount === 1 ? 1 : undefined}
draggable={false}
action=""
fileList={[]}
onFileChange={handleUpload}
disabled={isDisabled}
multiple={limitFileCount > 1}
>
{children}
</Upload>
);
};
ChatUpload.displayName = 'UiKitChatUpload';

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { Divider, Typography } from '@coze-arch/coze-design';
interface ContextDividerProps {
className?: string;
text?: string;
}
export const ContextDivider = ({ text, className }: ContextDividerProps) => (
<Divider className={classNames(className, 'w-full my-24px ')} align="center">
<Typography.Paragraph
ellipsis={{
showTooltip: {
opts: {
content: text,
style: {
wordBreak: 'break-word',
},
},
},
rows: 2,
}}
className="coz-fg-dim whitespace-pre-wrap text-center text-base leading-[16px] font-normal break-words"
>
{text}
</Typography.Paragraph>
</Divider>
);

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './chat-input';
export * from './chat-upload';
export * from './on-boarding';
export * from './stop-respond-button';
export * from './thinking-placeholder';
export * from './context-divider';
export * from './with-rule-img-background';
export * from './audio-record';
export type { UiKitChatInputButtonConfig } from './chat-input/index';

View File

@@ -0,0 +1,63 @@
.chat-uikit-on-boarding {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
// max-width: 576px;
height: 100%;
padding: 0 10px;
&__bot {
display: flex;
flex-direction: column;
column-gap: 20px;
align-items: center;
justify-content: center;
width: 100%;
&.chat-uikit-on-boarding__bot__with__onboarding {
row-gap: 12px;
// justify-content: flex-start;
}
}
&__prologue-sug {
width: fit-content;
max-width: 100%;
}
&__prologue {
user-select: text;
width: fit-content;
max-width: 100%;
margin-top: 16px;
font-style: normal;
word-break: break-word;
}
&__suggestions {
display: flex;
flex-direction: column;
align-self: flex-start;
max-width: 100%;
}
}
// pc store页面开场白视觉与输入框对齐整体左偏差
.chat-uikit-on-boarding-pc {
.chat-uikit-on-boarding__bot {
padding-right: 14px;
padding-left: 58px;
}
.chat-uikit-on-boarding__prologue-sug {
padding-right: 14px;
padding-left: 58px;
}
}

View File

@@ -0,0 +1,270 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type CSSProperties,
useState,
forwardRef,
type FC,
useContext,
} from 'react';
import classNames from 'classnames';
import { useUpdateEffect } from 'ahooks';
import { Avatar, Typography } from '@coze-arch/coze-design';
import {
MdBoxLazy,
type MdBoxLazyProps,
} from '@coze-arch/bot-md-box-adapter/lazy';
import {
Layout,
type IMessage,
type IEventCallbacks,
} from '@coze-common/chat-uikit-shared';
import { CozeLink } from '../../md-box-slots/link';
import {
type CozeImageProps,
CozeImageWithPreview,
} from '../../md-box-slots/coze-image';
import { SuggestionItem } from '../../contents/suggestion-content/components/suggestion-item';
import { OnboardingContext } from '../../../context/onboarding';
import { NO_MESSAGE_ID_MARK } from '../../../constants/grab';
import defaultAvatar from '../../../assets/default-square-avatar.png';
import {
typeSafeBotInfoNameVariants,
type BotInfoVariantProps,
} from './variants';
import './index.less';
interface OnBoardingProps {
avatar?: string;
name?: string;
prologue?: string;
suggestionList?: IMessage[];
/**
* suggestionList是否换行展示, 默认false
*/
suggestionsWrap?: boolean;
readonly?: boolean;
suggestionListWithString?: string[];
/**
* suggestionListWithString是否换行展示, 默认false
*/
suggestionsWithStringWrap?: boolean;
onSuggestionClick?: (content: string) => void;
className?: string;
prologueClassName?: string;
mdBoxProps?: Pick<MdBoxLazyProps, 'insertedElements' | 'slots'>;
style?: CSSProperties;
showBackground?: boolean;
layout?: Layout;
enableAutoSizeImage?: boolean;
imageAutoSizeContainerWidth?: number;
eventCallbacks?: IEventCallbacks;
suggestionItemColor?: 'white' | 'grey';
}
interface BotInfoProps {
wrapperClassName?: string;
avatar: string | undefined;
onError: () => void;
name: string | undefined;
}
const BotInfo: React.FC<BotInfoProps & BotInfoVariantProps> = ({
avatar,
wrapperClassName,
onError,
name,
showBackground,
}) => (
<div className={wrapperClassName}>
<Avatar
className={classNames('h-[64px] w-[64px]', 'rounded-[16px]')}
src={avatar}
shape="square"
onError={onError}
></Avatar>
{name ? (
<Typography.Text
ellipsis
className={typeSafeBotInfoNameVariants({
showBackground: Boolean(showBackground),
})}
>
{name}
</Typography.Text>
) : null}
</div>
);
export const OnBoarding = forwardRef<HTMLDivElement, OnBoardingProps>(
(props, ref) => {
const {
avatar,
name,
prologue,
suggestionList,
readonly,
suggestionListWithString,
onSuggestionClick,
className,
prologueClassName,
mdBoxProps,
style,
showBackground,
layout,
enableAutoSizeImage,
imageAutoSizeContainerWidth,
eventCallbacks,
suggestionsWrap = false,
suggestionsWithStringWrap = false,
suggestionItemColor,
} = props;
const [botAvatar, setBotAvatar] = useState(avatar || defaultAvatar);
const suggestions = suggestionList || suggestionListWithString;
const isOnboardingEmpty = !prologue && !suggestions?.length;
useUpdateEffect(() => {
setBotAvatar(avatar || defaultAvatar);
}, [avatar]);
return (
<OnboardingContext.Provider
value={{
imageAutoSizeContainerWidth,
eventCallbacks,
}}
>
<div
ref={ref}
className={classNames('chat-uikit-on-boarding', className, {
'chat-uikit-on-boarding-pc': layout === Layout.PC,
})}
style={style}
>
<BotInfo
wrapperClassName={classNames(
'chat-uikit-on-boarding__bot',
!isOnboardingEmpty &&
'chat-uikit-on-boarding__bot__with__onboarding',
)}
avatar={botAvatar}
name={name}
showBackground={showBackground}
onError={() => setBotAvatar(defaultAvatar)}
/>
<div className={classNames('chat-uikit-on-boarding__prologue-sug')}>
{prologue ? (
<div
className={classNames(
[
'py-12px',
'px-16px',
layout === Layout.MOBILE ? 'text-[16px]' : 'text-lg',
'leading-[20px]',
'rounded-normal',
'bg-[var(--coz-mg-primary)]',
],
'chat-uikit-on-boarding__prologue',
prologueClassName,
{
'!coz-bg-image-bots !coz-stroke-image-bots': showBackground,
},
)}
data-grab-mark={NO_MESSAGE_ID_MARK}
>
<MdBoxLazy
markDown={prologue}
autoFixSyntax={{ autoFixEnding: false }}
slots={{
Image: enableAutoSizeImage
? CozeImageWithSizeProps
: undefined,
Link: CozeLink,
}}
{...mdBoxProps}
></MdBoxLazy>
</div>
) : null}
{Boolean(suggestionList?.length) && (
<div
className={classNames(
'chat-uikit-on-boarding__suggestions',
'mt-8px',
{
'flex-wrap !flex-row gap-2': suggestionsWrap,
},
)}
>
{suggestionList?.map((message, index) => (
<SuggestionItem
key={index}
className={classNames({
'!mb-0': suggestionsWrap,
})}
message={message}
readonly={readonly}
onSuggestionClick={({ text }) => onSuggestionClick?.(text)}
showBackground={showBackground}
color={suggestionItemColor}
></SuggestionItem>
))}
</div>
)}
{Boolean(suggestionListWithString?.length) && (
<div
className={classNames(
'chat-uikit-on-boarding__suggestions',
'mt-8px',
{
'flex-wrap !flex-row gap-2': suggestionsWithStringWrap,
},
)}
>
{suggestionListWithString?.map((content, index) => (
<SuggestionItem
key={index}
className={classNames({
'!mb-0': suggestionsWithStringWrap,
})}
content={content}
readonly={readonly}
onSuggestionClick={({ text }) => onSuggestionClick?.(text)}
showBackground={showBackground}
color={suggestionItemColor}
></SuggestionItem>
))}
</div>
)}
</div>
</div>
</OnboardingContext.Provider>
);
},
);
const CozeImageWithSizeProps: FC<CozeImageProps> = props => {
const { imageAutoSizeContainerWidth } = useContext(OnboardingContext);
return (
<CozeImageWithPreview
{...props}
imageAutoSizeContainerWidth={imageAutoSizeContainerWidth}
/>
);
};
CozeImageWithSizeProps.displayName = 'CozeImageWithSizeProps';

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cva, type VariantProps } from 'class-variance-authority';
export const botInfoNameVariants = cva(
['leading-[28px]', 'font-medium', 'text-20px'],
{
variants: {
showBackground: {
true: ['coz-fg-images-user-name'],
false: ['coz-fg-plus'],
},
},
},
);
export type BotInfoVariantProps = VariantProps<typeof botInfoNameVariants>;
export const typeSafeBotInfoNameVariants: (
props: BotInfoVariantProps,
) => string = botInfoNameVariants;

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import classNames from 'classnames';
import { IconCozStopCircle } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
interface IProps {
className?: string;
content: string;
onClick?: () => void;
}
export const StopRespondButton: FC<IProps> = props => {
const { content, onClick, className } = props;
return (
<Button
color="secondary"
onClick={onClick}
className={classNames(
'coz-stroke-primary',
'coz-fg-primary',
'border-[1px]',
'border-solid',
'coz-shadow-default',
className,
)}
icon={<IconCozStopCircle />}
>
{content}
</Button>
);
};
StopRespondButton.displayName = 'StopRespondButton';

View File

@@ -0,0 +1,62 @@
@keyframes chat-uikit-thinking-placeholder-animation {
0% {
background-color: var(--coz-fg-plus);
}
50% {
background-color: var(--coz-fg-dim);
}
100% {
background-color: var(--coz-fg-plus);
}
}
.chat-uikit-coz-thinking-placeholder {
position: relative;
overflow: visible;
width: 6px;
height: 6px;
margin: 0 10px;
@apply coz-fg-dim;
background-color: var(--coz-fg-dim);
border-radius: 100%;
animation: chat-uikit-thinking-placeholder-animation 0.8s infinite alternate;
animation-timing-function: ease;
animation-delay: -0.2s;
&::before,
&::after {
content: '';
position: absolute;
top: 0;
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--coz-fg-dim);
border-radius: 100%;
animation: chat-uikit-thinking-placeholder-animation 0.8s infinite alternate;
animation-timing-function: ease;
}
&::before {
left: -10px;
animation-delay: -0.4s;
}
&::after {
left: 10px;
animation-delay: 0s;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import classNames from 'classnames';
import {
type ThinkingPlaceholderVariantProps,
typeSafeThinkingPlaceholderVariants,
} from './variant';
import { type IThinkingPlaceholderProps } from './type';
import './animation.less';
const getVariantByProps = ({
theme,
showBackground,
}: {
theme: IThinkingPlaceholderProps['theme'];
showBackground: boolean;
}): ThinkingPlaceholderVariantProps => {
if (showBackground) {
return { backgroundColor: 'withBackground' };
}
if (!theme) {
return { backgroundColor: null };
}
return { backgroundColor: theme };
};
export const ThinkingPlaceholder: FC<IThinkingPlaceholderProps> = props => {
const { className, theme = 'none', showBackground } = props;
return (
<div
className={classNames(
typeSafeThinkingPlaceholderVariants(
getVariantByProps({ showBackground: Boolean(showBackground), theme }),
),
className,
)}
>
<div className="chat-uikit-coz-thinking-placeholder"></div>
</div>
);
};

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface IThinkingPlaceholderProps {
className?: string;
theme?: 'primary' | 'whiteness' | 'none' | 'grey';
showBackground?: boolean;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cva, type VariantProps } from 'class-variance-authority';
const thinkingPlaceholderVariants = cva(
[
'h-[44px]',
'w-fit',
'flex',
'justify-center',
'items-center',
'py-12px',
'px-16px',
'rounded-normal',
],
{
variants: {
backgroundColor: {
whiteness: ['bg-[var(--coz-mg-card)]'],
grey: ['bg-[var(--coz-mg-primary)]'],
primary: ['bg-[var(coz-mg-hglt-plus)]'],
withBackground: ['coz-bg-image-bots', 'coz-stroke-image-bots'],
none: ['coz-stroke-primary'],
},
},
},
);
export type ThinkingPlaceholderVariantProps = Required<
VariantProps<typeof thinkingPlaceholderVariants>
>;
export const typeSafeThinkingPlaceholderVariants: (
props: ThinkingPlaceholderVariantProps,
) => string = thinkingPlaceholderVariants;

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const MODE_CONFIG = {
pc: {
size: {
width: 486,
height: 346,
},
centerWidth: 346,
},
mobile: {
size: {
width: 248,
height: 346,
},
centerWidth: 206,
},
};

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react';
import { useSize } from 'ahooks';
import { getStandardRatio } from '../utils';
import { type BackgroundImageInfo } from '../types';
import { MODE_CONFIG } from '../const';
export const useGetResponsiveBackgroundInfo = ({
backgroundInfo,
}: {
backgroundInfo?: BackgroundImageInfo;
}) => {
const targetRef = useRef(null);
const size = useSize(targetRef);
const { width = 0, height = 0 } = size ?? {};
const isMobileMode = width / height <= getStandardRatio('mobile');
const mobileBackgroundInfo = backgroundInfo?.mobile_background_image;
const pcBackgroundInfo = backgroundInfo?.web_background_image;
const currentBackgroundInfo = isMobileMode
? mobileBackgroundInfo
: pcBackgroundInfo;
const { theme_color } = currentBackgroundInfo ?? {};
const { size: cropperSize } = MODE_CONFIG[isMobileMode ? 'mobile' : 'pc'];
return {
targetRef,
currentBackgroundInfo,
targetWidth: width,
targetHeight: height,
currentThemeColor: theme_color,
cropperSize,
};
};

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect } from 'react';
import { isEmpty } from 'lodash-es';
import classNames from 'classnames';
import { addAlpha, computeShowGradient } from './utils';
import {
type CanvasPosition,
type GradientPosition,
type BackgroundImageInfo,
} from './types';
import { useGetResponsiveBackgroundInfo } from './hooks/use-get-background-info';
export interface WithRuleImgBackgroundProps {
preview?: boolean;
backgroundInfo?: BackgroundImageInfo;
onError?: () => void;
}
export const Gradient: React.FC<{
position: number;
preview: boolean;
showGradient: boolean;
background: string;
direction: 'left' | 'right';
}> = ({ position, preview, showGradient, background, direction }) => (
<div
className={classNames('absolute -translate-y-1/2 top-1/2 h-full z-10', {
'transition-all duration-500': !preview,
})}
style={{
[direction]: `${(position > 0 ? position : 0) * 100 - 0.1}%`, // 0.1为阴影补偿,防止出现间隙
width: '10%',
background,
opacity: showGradient ? 1 : 0,
}}
></div>
);
const getGradient = (
gradient: GradientPosition,
canvasData: CanvasPosition,
cropperWidth = 1,
) => {
const { left: cropperImgLeft = 0, width: cropperImgWidth = 0 } = canvasData;
// 伪裁剪 兼容历史gradient不准的问题
if (!isEmpty(canvasData)) {
return {
left: cropperImgLeft / cropperWidth,
right: (cropperWidth - cropperImgWidth - cropperImgLeft) / cropperWidth,
};
} else {
return gradient;
}
};
export const WithRuleImgBackground: React.FC<WithRuleImgBackgroundProps> = ({
preview = false,
backgroundInfo,
}) => {
const {
currentBackgroundInfo,
targetHeight,
targetWidth,
targetRef,
cropperSize,
} = useGetResponsiveBackgroundInfo({
backgroundInfo,
});
const {
theme_color,
gradient_position = {},
canvas_position = {},
} = currentBackgroundInfo ?? {};
const { left: gradientLeft = 0, right: gradientRight = 0 } = getGradient(
gradient_position,
canvas_position,
cropperSize.width,
);
const { top: cropperImgTop = 0, height: cropperImgHeight = 0 } =
canvas_position;
const [themeColor, setThemeColor] = useState(theme_color ?? 'transparent');
// 与裁剪框等比例算出图片渲染区域的宽度
const imgWidth = (targetHeight * cropperSize.width) / cropperSize.height;
const mediumColor = addAlpha(themeColor, 0.95);
useEffect(() => {
if (theme_color) {
setThemeColor(theme_color);
}
}, [currentBackgroundInfo]);
return (
<div
data-testid="chat.with_rule_img_background"
ref={targetRef}
className={
'rule-img-background absolute left-1/2 -translate-x-1/2 w-full h-full overflow-hidden pointer-events-none'
}
style={{
background: preview ? 'none' : themeColor,
zIndex: preview ? 100 : 0,
}}
>
{/* 背景图上压黑阴影 */}
<div className="bg-[rgba(0,0,0,0.12)] absolute w-full h-full z-[200] rounded-t-[6px]"></div>
<div className="relative w-fit h-fit left-1/2 -translate-x-1/2">
{
<Gradient
preview={preview}
showGradient={computeShowGradient(
targetWidth,
imgWidth,
gradientLeft,
)}
position={gradientLeft}
direction="left"
background={`linear-gradient(90deg, ${themeColor} 10%, ${mediumColor} 28%, transparent 92.4%)`}
/>
}
{preview ? (
<div
style={{
height: targetHeight,
width: targetWidth,
}}
></div>
) : (
<div
style={{
width: imgWidth,
height: targetHeight,
position: 'relative',
overflow: 'hidden',
}}
>
<img
src={currentBackgroundInfo?.origin_image_url}
alt=""
style={{
height: `${(cropperImgHeight / cropperSize.height) * 100}%`,
position: 'absolute',
left: `${
gradientLeft ? gradientLeft * 100 : -gradientRight * 2 * 100
}%`,
top: `${(cropperImgTop / cropperSize.height) * 100}%`,
}}
/>
</div>
)}
{
<Gradient
preview={preview}
showGradient={computeShowGradient(
targetWidth,
imgWidth,
gradientRight,
)}
position={gradientRight}
direction="right"
background={`linear-gradient(90deg, transparent 10% , ${mediumColor} 72%, ${themeColor} 92%)`}
/>
}
</div>
</div>
);
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface GradientPosition {
left?: number;
right?: number;
}
export interface CanvasPosition {
width?: number;
height?: number;
left?: number;
top?: number;
}
export interface BackgroundImageDetail {
/** 原始图片 */
origin_image_uri?: string;
origin_image_url?: string;
/** 实际使用图片 */
image_uri?: string;
image_url?: string;
theme_color?: string;
/** 渐变位置 */
gradient_position?: GradientPosition;
/** 裁剪画布位置 */
canvas_position?: CanvasPosition;
}
export interface BackgroundImageInfo {
/** web端背景图 */
web_background_image?: BackgroundImageDetail;
/** 移动端背景图 */
mobile_background_image?: BackgroundImageDetail;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { MODE_CONFIG } from './const';
// 输入透明度系数 和color 返回新的颜色
export function addAlpha(color: string, alpha: number): string {
const regex = /^rgba\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/;
if (!regex.test(color)) {
return color;
}
const values: string[] = color.slice(5, -1).split(',');
values.push(alpha.toString());
const newColor = `rgba(${values.join(',')})`;
return newColor;
}
// 图片的宽高比
export const getStandardRatio = (mode: 'pc' | 'mobile'): number =>
MODE_CONFIG[mode].size.width / MODE_CONFIG[mode].size.height;
// 计算是否展示渐变阴影 = 屏幕宽度 > 图片宽度 * 1- 2 * 左/右阴影位置)
export const computeShowGradient = (
width: number,
imgWidth: number,
percent: number,
): boolean => width > imgWidth * (1 - (percent > 0 ? percent : 0) * 2);

View File

@@ -0,0 +1,86 @@
.button {
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 12px;
color: #4D53E8;
background: #FFF;
border: 1px solid rgba(10, 17, 61, 6%);
border-radius: 16px;
}
.button:hover {
background: linear-gradient(0deg, rgba(0, 0, 0, 4%) 0%, rgba(0, 0, 0, 4%) 100%), #FFF;
}
.button:active {
background: linear-gradient(0deg, rgba(0, 0, 0, 8%) 0%, rgba(0, 0, 0, 8%) 100%), #FFF;
}
.button-text {
margin-left: 8px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: #4D53E8;
}
.outlined-icon-button {
@apply !coz-bg-max;
&:hover {
@apply !coz-mg-secondary-hovered;
}
&:active {
@apply !coz-mg-secondary-pressed;
}
&:disabled {
@apply !coz-fg-dim bg-transparent;
&:hover,
&:active {
@apply !coz-fg-dim bg-transparent;
}
}
}
.outlined-icon-button-background {
&:hover {
/* stylelint-disable-next-line declaration-no-important */
background: rgba(249, 249, 249, 90%) !important;
}
&:active {
/* stylelint-disable-next-line declaration-no-important */
background: rgba(249, 249, 249, 85%) !important;
}
&:disabled {
@apply !coz-fg-dim bg-transparent;
&:hover,
&:active {
@apply !coz-fg-dim bg-transparent;
}
}
}
.base-outlined-icon-button {
@apply !coz-fg-primary !coz-stroke-plus;
width: 32px;
// 和 ui 确认后的业务定制样式
/* stylelint-disable-next-line declaration-no-important */
border-style: solid !important;
/* stylelint-disable-next-line declaration-no-important */
border-width: 1px !important;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forwardRef } from 'react';
import classNames from 'classnames';
import { IconButton, type ButtonProps } from '@coze-arch/coze-design';
import { type Button as SemiButton } from '@douyinfe/semi-ui';
import styles from './index.module.less';
export const OutlinedIconButton = forwardRef<
SemiButton,
ButtonProps & { showBackground: boolean }
>(({ className, showBackground, ...restProps }, ref) => (
<IconButton
ref={ref}
className={classNames(
className,
showBackground
? ['!coz-bg-image-bots', styles['outlined-icon-button-background']]
: styles['outlined-icon-button'],
styles['base-outlined-icon-button'],
)}
{...restProps}
/>
));

View File

@@ -0,0 +1,274 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode, type FC } from 'react';
import {
type IEventCallbacks,
ContentBoxType,
type MdBoxProps,
type GetBotInfo,
type Layout,
type IContentConfigs,
type IMessage,
} from '@coze-common/chat-uikit-shared';
import { ContentType } from '@coze-common/chat-core';
import { TextContent } from '../../contents/text-content';
import { SingleImageContentWithAutoSize } from '../../contents/single-image-content/auto-size';
import { SingleImageContent } from '../../contents/single-image-content';
import { SimpleFunctionContent } from '../../contents/simple-function-content';
import { PlainTextContent } from '../../contents/plain-text-content';
import { MultimodalContent } from '../../contents/multimodal-content';
import { ImageContent } from '../../contents/image-content';
import { FileContent } from '../../contents/file-content';
import { isImage } from '../../../utils/is-image';
import { defaultEnable } from '../../../utils/default-enable';
import { MESSAGE_TYPE_VALID_IN_TEXT_LIST } from '../../../constants/content-box';
export interface EnhancedContentConfig {
rule: (params: {
message: IMessage;
contentType: ContentType;
contentConfigs: IContentConfigs | undefined;
}) => boolean;
render: (params: {
message: IMessage;
eventCallbacks: IEventCallbacks | undefined;
contentConfigs: IContentConfigs | undefined;
options: {
isCardDisabled?: boolean;
isContentLoading?: boolean;
showBackground: boolean;
readonly?: boolean;
};
}) => ReactNode;
}
export interface IContentBoxProps {
/**
* Core SDK的消息体内容
*/
message: IMessage;
/**
* 事件回调对象
*/
eventCallbacks?: IEventCallbacks;
/**
* content卡片配置的内容
*/
contentConfigs?: IContentConfigs;
/**
* 是否只读
*/
readonly?: boolean;
getBotInfo: GetBotInfo;
layout: Layout;
/**
* 在 mix 模式下,给 text 格式卡片加插槽
*/
multimodalTextContentAddonTop?: ReactNode;
showBackground: boolean;
/**
* 启用自动适应图片能力
*/
enableAutoSizeImage?: boolean;
/**
* mdBox的配置
*/
mdBoxProps?: MdBoxProps;
/**
* 卡片状态是否为disabled
*/
isCardDisabled?: boolean;
isContentLoading?: boolean;
enhancedContentConfigList?: EnhancedContentConfig[];
}
// eslint-disable-next-line complexity, @coze-arch/max-line-per-function, @coze-arch/max-line-per-function
export const ContentBox: FC<IContentBoxProps> = props => {
const {
message,
contentConfigs,
readonly,
getBotInfo,
layout,
showBackground,
enableAutoSizeImage,
isCardDisabled,
isContentLoading,
enhancedContentConfigList,
} = props;
/**
* Content内容启用配置 Start
*/
const isTextEnable = defaultEnable(
contentConfigs?.[ContentBoxType.TEXT]?.enable,
);
const isImageEnable = defaultEnable(
contentConfigs?.[ContentBoxType.IMAGE]?.enable,
);
const isFileEnable = contentConfigs?.[ContentBoxType.FILE]?.enable;
const isSimpleFunctionEnable =
contentConfigs?.[ContentBoxType.SIMPLE_FUNCTION]?.enable;
/**
* Content内容启用配置 End
*/
const enhancedContentConfig = enhancedContentConfigList?.find(config =>
config.rule({ contentType: message.content_type, contentConfigs, message }),
);
if (enhancedContentConfig) {
return enhancedContentConfig.render({
message,
eventCallbacks: props.eventCallbacks,
contentConfigs,
options: { isCardDisabled, isContentLoading, showBackground, readonly },
});
}
/**
* 文本类型的处理
* 这里目前有两种情况第一种message.type = 'follow_up' 代表是suggestion 第二种反之是普通文本消息
*/
if (message.content_type === ContentType.Text) {
const { eventCallbacks, mdBoxProps } = props;
const { onImageClick, onLinkClick } = eventCallbacks ?? {};
if (
MESSAGE_TYPE_VALID_IN_TEXT_LIST.includes(message.type) &&
isTextEnable
) {
return message.role === 'user' ? (
<PlainTextContent
isContentLoading={isContentLoading}
content={message.content}
getBotInfo={getBotInfo}
mentioned={message.mention_list.at(0)}
/>
) : (
<TextContent
message={message}
readonly={readonly}
onImageClick={onImageClick}
onLinkClick={onLinkClick}
enableAutoSizeImage={enableAutoSizeImage}
mdBoxProps={mdBoxProps}
/>
);
}
}
/**
* FIle类型的内容
*/
if (message.content_type === ContentType.File && isFileEnable) {
const { copywriting, fileAttributeKeys } =
contentConfigs[ContentBoxType.FILE] ?? {};
const { eventCallbacks } = props;
const { onCancelUpload, onCopyUpload, onRetryUpload } =
eventCallbacks ?? {};
return (
<FileContent
message={message}
copywriting={copywriting}
fileAttributeKeys={fileAttributeKeys}
readonly={readonly}
onCancel={onCancelUpload}
onCopy={onCopyUpload}
onRetry={onRetryUpload}
layout={layout}
showBackground={showBackground}
/>
);
}
/**
* 图片类型的内容
*/
if (message.content_type === ContentType.Image && isImageEnable) {
const { eventCallbacks } = props;
const { onImageClick } = eventCallbacks ?? {};
if (!isImage(message.content_obj)) {
return null;
}
const UsedSingleImageContent = enableAutoSizeImage
? SingleImageContentWithAutoSize
: SingleImageContent;
const isMultipleImage = message.content_obj.image_list.length > 1;
if (isMultipleImage) {
return <ImageContent message={message} onImageClick={onImageClick} />;
}
return (
<UsedSingleImageContent message={message} onImageClick={onImageClick} />
);
}
/**
* function类型
*/
if (message.type === 'function_call' && isSimpleFunctionEnable) {
const { copywriting } =
contentConfigs[ContentBoxType.SIMPLE_FUNCTION] ?? {};
return (
<SimpleFunctionContent message={message} copywriting={copywriting} />
);
}
/**
* 文件文字同时发送的多模态消息
*/
if (
message.content_type === ContentType.Mix &&
isFileEnable &&
isImageEnable &&
isTextEnable
) {
const { copywriting, fileAttributeKeys } =
contentConfigs[ContentBoxType.FILE] ?? {};
const { eventCallbacks } = props;
const { onCancelUpload, onCopyUpload, onRetryUpload, onImageClick } =
eventCallbacks ?? {};
return (
<MultimodalContent
isContentLoading={isContentLoading}
renderTextContentAddonTop={props.multimodalTextContentAddonTop}
message={message}
getBotInfo={getBotInfo}
fileAttributeKeys={fileAttributeKeys}
copywriting={copywriting}
readonly={readonly}
onCancel={onCancelUpload}
onCopy={onCopyUpload}
onRetry={onRetryUpload}
onImageClick={onImageClick}
layout={layout}
showBackground={showBackground}
/>
);
}
return <span>Not Support {message.content_type} Content</span>;
};
ContentBox.displayName = 'UIKitContentBox';

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ComponentPropsWithRef, type FC } from 'react';
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
import { CozeLink } from '../../md-box-slots/link';
import { CozeImage } from '../../md-box-slots/coze-image';
export const LazyCozeMdBox: FC<
ComponentPropsWithRef<typeof MdBoxLazy>
> = props => (
<MdBoxLazy
slots={{
Image: CozeImage,
Link: CozeLink,
}}
{...props}
/>
);

View File

@@ -0,0 +1,7 @@
.full-width-aligner {
pointer-events: none;
}
.full-width-aligner-inner-wrap {
pointer-events: auto;
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { PropsWithChildren } from 'react';
import classNames from 'classnames';
import './index.less';
/**
* 套壳组件,默认宽度通栏,用于帮助孤立组件与 message box 保持宽度对齐
*/
export const FullWidthAligner = (
props: PropsWithChildren<{
alignWidth?: string;
className?: string;
innerWrapClassName?: string;
}>,
) => {
const { alignWidth, children, className, innerWrapClassName } = props;
return (
<div
className={classNames('full-width-aligner', className)}
style={{
width: alignWidth || '100%',
}}
>
<span
className={classNames(
'full-width-aligner-inner-wrap',
innerWrapClassName,
)}
>
{children}
</span>
</div>
);
};
FullWidthAligner.displayName = 'UIKitFullWidthAligner';

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './content-box';
export * from './tooltips';
export * from './message-box';
export * from './button';

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren, type FC } from 'react';
export const DefaultAvatarWrap: FC<PropsWithChildren> = ({ children }) => (
<>{children}</>
);

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef, useEffect, type ComponentType } from 'react';
import { type FallbackProps } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { useUiKitMessageBoxContext } from '../../../context/message-box';
export const FallbackComponent: ComponentType<FallbackProps> = ({ error }) => {
const { onError } = useUiKitMessageBoxContext();
const reported = useRef(false);
useEffect(() => {
if (!onError || !error) {
return;
}
if (reported.current) {
return;
}
onError(error);
reported.current = true;
}, [onError, error]);
return (
<div className="p-[12px]">
<span className="text-[14px] font-medium text-[#222222]">
{I18n.t('message_content_error')}
</span>
</div>
);
};

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { Layout } from '@coze-common/chat-uikit-shared';
import { ContentBox } from '../content-box';
import {
type MessageBoxProps,
type MessageBoxShellProps,
type NormalMessageBoxProps,
} from './type';
import { MessageBoxWrap } from './message-box-wrap';
export const MessageBox: FC<
MessageBoxShellProps | NormalMessageBoxProps
> = props => {
const {
theme = 'none',
renderFooter,
hoverContent,
senderInfo,
showUserInfo,
right,
classname,
messageBubbleClassname,
messageBubbleWrapperClassname,
messageBoxWraperClassname,
messageErrorWrapperClassname,
isHoverShowUserInfo,
layout = Layout.PC,
showBackground = false,
topRightSlot,
imageAutoSizeContainerWidth,
enableImageAutoSize,
messageId,
eventCallbacks,
onError,
} = props ?? {};
const { url, nickname, id, userLabel, userUniqueName } = senderInfo ?? {};
return (
<MessageBoxWrap
messageId={messageId}
theme={theme}
avatar={url}
nickname={nickname}
showUserInfo={showUserInfo}
renderFooter={renderFooter}
hoverContent={hoverContent}
right={right}
senderId={id || ''}
classname={classname}
messageBubbleWrapperClassname={messageBubbleWrapperClassname}
messageBubbleClassname={messageBubbleClassname}
messageBoxWraperClassname={messageBoxWraperClassname}
messageErrorWrapperClassname={messageErrorWrapperClassname}
isHoverShowUserInfo={isHoverShowUserInfo}
layout={layout}
contentTime={getMessageContentTime(props)}
showBackground={showBackground}
extendedUserInfo={{
userLabel,
userUniqueName,
}}
topRightSlot={topRightSlot}
imageAutoSizeContainerWidth={imageAutoSizeContainerWidth}
enableImageAutoSize={enableImageAutoSize}
eventCallbacks={eventCallbacks}
onError={onError}
>
{getMessageBoxContent(props)}
</MessageBoxWrap>
);
};
const getMessageContentTime = (props: MessageBoxProps): number | undefined => {
if ('message' in props) {
return Number(props.message.content_time);
}
};
const getMessageBoxContent = (props: MessageBoxProps) => {
if ('children' in props) {
return props.children;
}
const {
message,
contentConfigs,
eventCallbacks,
getBotInfo,
layout = Layout.PC,
showBackground = false,
isContentLoading,
isCardDisabled,
} = props;
return (
<ContentBox
message={message}
contentConfigs={contentConfigs}
eventCallbacks={eventCallbacks}
getBotInfo={getBotInfo}
layout={layout}
showBackground={showBackground}
isContentLoading={isContentLoading}
isCardDisabled={isCardDisabled}
/>
);
};
MessageBox.displayName = 'UIKitMessageBox';

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type PropsWithChildren,
type FC,
useRef,
useState,
useEffect,
} from 'react';
import classnames from 'classnames';
import { useClickAway, useHover, useUpdateEffect } from 'ahooks';
import { ErrorBoundary } from '@coze-arch/logger';
import {
Layout,
UIKitEvents,
useUiKitEventCenter,
} from '@coze-common/chat-uikit-shared';
import { useEventCallback } from '@coze-common/chat-hooks';
import { Avatar, Typography } from '@coze-arch/coze-design';
import { UserLabel, UserName } from '../user-label';
import { MessageContentTime } from '../message-content-time';
import { typeSafeMessageBoxInnerVariants } from '../../../variants/message-box-inner-variants';
import { useObserveCardContainer } from '../../../hooks/use-observe-card-container';
import { UIKitMessageBoxProvider } from '../../../context/message-box';
import { useUIKitCustomComponent } from '../../../context/custom-components';
import defaultAvatar from '../../../assets/default-avatar.png';
import {
typeSafeBotNicknameVariants,
messageBoxContainerVariants,
} from './variants';
import { getMessageBoxInnerVariantsByTheme } from './utils';
import { type MessageBoxWrapProps } from './type';
import { FallbackComponent } from './fallback';
import { DefaultAvatarWrap } from './default-avatar-wrap';
import './message-box.less';
export const MessageBoxWrap: FC<
PropsWithChildren<MessageBoxWrapProps>
// eslint-disable-next-line @coze-arch/max-line-per-function
> = props => {
const {
children,
theme,
nickname,
avatar,
showUserInfo,
renderFooter,
hoverContent,
right,
senderId,
classname,
messageBubbleClassname,
messageBubbleWrapperClassname,
messageBoxWraperClassname,
messageErrorWrapperClassname,
isHoverShowUserInfo = true,
layout,
contentTime,
showBackground,
extendedUserInfo,
topRightSlot,
imageAutoSizeContainerWidth,
enableImageAutoSize,
messageId,
eventCallbacks,
onError,
} = props;
const { userLabel, userUniqueName } = extendedUserInfo ?? {};
const [botAvatar, setBotAvatar] = useState(avatar || defaultAvatar);
const { MentionOperateTool = () => null, AvatarWrap = DefaultAvatarWrap } =
useUIKitCustomComponent();
const wrapRef = useRef<HTMLDivElement>(null);
const messageContainerRef = useRef<HTMLDivElement>(null);
const messageFooterRef = useRef<HTMLDivElement>(null);
const eventCenter = useUiKitEventCenter();
const isMobileLayout = layout === Layout.MOBILE;
const refreshContainerWidthConditionally = useEventCallback(() => {
if (!messageContainerRef.current || !messageFooterRef.current) {
return;
}
const currentMessageWidth = `${messageContainerRef.current.offsetWidth}px`;
const currentFooterWidth = messageFooterRef.current.style.width;
if (currentFooterWidth === currentMessageWidth) {
return;
}
messageFooterRef.current.style.width = currentMessageWidth;
});
useUpdateEffect(() => {
setBotAvatar(avatar || defaultAvatar);
}, [avatar]);
useObserveCardContainer({
messageId,
cardContainerRef: messageContainerRef,
onResize: refreshContainerWidthConditionally,
});
useEffect(() => {
if (!eventCenter) {
return;
}
eventCenter.on(
UIKitEvents.WINDOW_RESIZE,
refreshContainerWidthConditionally,
);
return () => {
eventCenter.off(
UIKitEvents.WINDOW_RESIZE,
refreshContainerWidthConditionally,
);
};
}, []);
const isHovering = useHover(() => wrapRef.current);
// 适配移动端 移动端没有hover效果使用点击交互
const [hoverContentVisible, setHoverContentVisible] =
useState<boolean>(false);
useClickAway(() => {
setHoverContentVisible(false);
}, wrapRef);
return (
<UIKitMessageBoxProvider
value={{
layout,
imageAutoSizeContainerWidth,
enableImageAutoSize,
eventCallbacks,
onError,
}}
>
<div
// chat-uikit-message-box
className={classnames('max-w-full', classname)}
ref={wrapRef}
onClick={() => {
if (isMobileLayout) {
setHoverContentVisible(true);
}
}}
>
<div
// chat-uikit-message-box-container chat-uikit-message-box-container-pc
className={classnames(
messageBoxContainerVariants({ isMobileLayout }),
messageBoxWraperClassname,
)}
>
<div
// chat-uikit-message-box-container__avatar-wrap
className="mr-[12px] w-32px h-32px"
>
{showUserInfo ? (
<AvatarWrap>
<Avatar
// chat-uikit-message-box-container__avatar-wrap__avatar
size="small"
src={botAvatar}
onError={() => setBotAvatar(defaultAvatar)}
></Avatar>
</AvatarWrap>
) : null}
</div>
<div
// chat-uikit-message-box-container__message
className="flex-1 max-w-[calc(100%-44px)]"
>
{/* TODO: 不支持一条消息内渲染多个 content */}
<div
// chat-uikit-message-box-container__message__message-box
className="relative flex flex-col w-fit max-w-full"
>
{showUserInfo && nickname ? (
<div
// chat-uikit-message-box-container__message__nickname
className="flex"
>
<Typography.Text
ellipsis={{
showTooltip: {
opts: {
content: nickname,
},
},
}}
// chat-uikit-message-box-container__message__nickname-text
className={typeSafeBotNicknameVariants({
showBackground: Boolean(showBackground),
})}
>
{nickname}
</Typography.Text>
<UserLabel userLabel={userLabel} />
<div
// chat-uikit-message-box-container__message__nickname-partner
className="flex shrink w-full h-[20px]"
>
{isHovering && isHoverShowUserInfo ? (
<>
<UserName
userUniqueName={userUniqueName}
showBackground={showBackground}
/>
<MentionOperateTool senderId={senderId} />
<MessageContentTime
contentTime={contentTime}
showBackground={Boolean(showBackground)}
className="flex-shrink-0"
/>
</>
) : null}
<div className="flex gap-x-[8px] ml-auto">
{topRightSlot}
</div>
</div>
</div>
) : null}
<div
ref={messageContainerRef}
// chat-uikit-message-box-container__message__message-box__content
className={classnames(
messageBubbleWrapperClassname,
'select-text relative flex flex-row w-fit max-w-full',
)}
>
<div
// className={classnames('chat-uikit-message-box-inner', {
// 'chat-uikit-message-box-inner--primary':
// theme === 'primary',
// 'chat-uikit-message-box-inner--whiteness':
// theme === 'whiteness',
// 'chat-uikit-message-box-inner--colorful':
// theme === 'colorful',
// 'chat-uikit-message-box-inner--border': theme === 'border',
// 'chat-uikit-message-box-inner--none': theme === 'none',
// '!coz-bg-image-user !coz-stroke-image-user':
// showBackground && theme === 'primary',
// '!coz-stroke-image-user !coz-bg-image-bots':
// showBackground && theme === 'border',
// '!coz-bg-image-bots !coz-stroke-image-bots':
// showBackground && theme === 'whiteness',
// 'chat-uikit-message-box-inner--color-border':
// theme === 'color-border',
// 'chat-uikit-message-box-inner--color-border-card':
// theme === 'color-border-card',
// })}
className={classnames(
messageBubbleClassname,
typeSafeMessageBoxInnerVariants({
showBackground: Boolean(showBackground),
...getMessageBoxInnerVariantsByTheme({ theme }),
}),
layout === Layout.MOBILE ? '!text-[16px]' : '',
)}
>
<ErrorBoundary
errorBoundaryName="chat-message-box-children"
FallbackComponent={FallbackComponent}
>
{children}
</ErrorBoundary>
</div>
<div
// chat-uikit-message-box-container__message__message-box__content__right
className={classnames(
'absolute right-0 bottom-[1px]',
messageErrorWrapperClassname,
)}
>
{right}
</div>
</div>
{/* 对这个 dom 的样式改动前请先阅读上方的 refreshContainerWidthConditionally 方法 */}
<div
ref={messageFooterRef}
// chat-uikit-message-box-container__message__message-box__footer
className="overflow-visible"
>
{renderFooter?.(refreshContainerWidthConditionally)}
</div>
{isHovering || hoverContentVisible ? (
<div
// chat-uikit-message-box-container__message__message-box__hover-container
className="absolute right-[-12px] bottom-[-20px]"
>
{hoverContent}
</div>
) : null}
</div>
</div>
</div>
</div>
</UIKitMessageBoxProvider>
);
};
MessageBoxWrap.displayName = 'UIKitMessageBoxWrap';

View File

@@ -0,0 +1,3 @@
.chat-uikit-message-box-bg-primary {
background: var(--coz-mg-hglt-plus-dim);
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import {
type IEventCallbacks,
type Layout,
type IContentConfigs,
type IMessage,
type GetBotInfo,
} from '@coze-common/chat-uikit-shared';
import { type UserLabelInfo } from '../user-label';
/**
* 样式主题
* @deprecated 考虑替换实现方式,目前使用不够灵活
*/
export type MessageBoxTheme =
/** 主题色 */
| 'primary'
/** 白色背景 */
| 'whiteness'
/**
* 灰色背景
*/
| 'grey'
/** home用的炫彩底色 */
| 'colorful'
/** 官方通知,有彩色边框 */
| 'color-border'
/** 官方通知,有彩色边框,但是不留 padding */
| 'color-border-card'
| 'border'
| 'none';
interface MessageBoxBasicProps {
/**
* 用户信息
*/
senderInfo: {
userUniqueName?: string;
nickname?: string;
url?: string;
id: string;
userLabel?: UserLabelInfo | null;
};
/**
* 消息 id
*/
messageId: string | null;
showUserInfo?: boolean;
/**
* 主题
*/
theme?: MessageBoxTheme;
/**
* 插入消息的footer
*/
renderFooter?: (refreshContainerWidth: () => void) => React.ReactNode;
/** 鼠标悬浮时展示的组件 */
hoverContent?: ReactNode;
/**
* 左侧插槽
*/
right?: React.ReactNode;
/**
* 右上角插槽
*/
topRightSlot?: React.ReactNode;
getBotInfo: GetBotInfo;
/**
* 是否是移动端
*/
layout?: Layout;
classname?: string;
messageBubbleWrapperClassname?: string;
messageBoxWraperClassname?: string; // message box的直接父亲样式
messageBubbleClassname?: string; // message消息气泡的样式
messageErrorWrapperClassname?: string; // message错误的父亲样式
isHoverShowUserInfo?: boolean; // hover的时候是否显示用户详细信息
showBackground?: boolean;
/**
* 容器动态宽度,用于动态计算图片尺寸
*/
imageAutoSizeContainerWidth?: number;
/**
* 是否启用图片自适应模式
*/
enableImageAutoSize?: boolean;
/**
* 事件回调
*/
eventCallbacks?: IEventCallbacks;
/**
* 针对 JS Error 的响应
*/
onError?: (error: unknown) => void;
}
/** 只是套壳,内容由 children 呈现 */
export interface MessageBoxShellProps extends MessageBoxBasicProps {
children: React.ReactNode;
}
/** 含有完整内置实现的 MessageBox */
export interface NormalMessageBoxProps extends MessageBoxBasicProps {
/**
* 消息体
*/
message: IMessage;
/**
* 文件需要用到的必备参数
*/
contentConfigs?: IContentConfigs;
/** 样式主题 */
theme?: MessageBoxTheme;
footer?: ReactNode;
readonly?: boolean;
isContentLoading?: boolean;
isCardDisabled?: boolean;
}
export type MessageBoxProps = MessageBoxShellProps | NormalMessageBoxProps;
export interface MessageBoxWrapProps {
nickname?: string;
avatar?: string;
theme: MessageBoxTheme;
showUserInfo?: boolean;
renderFooter?: (refreshContainerWidth: () => void) => React.ReactNode;
/** 鼠标悬浮时展示的组件 */
hoverContent?: React.ReactNode;
right?: React.ReactNode;
/**
* 右上角插槽
*/
topRightSlot?: React.ReactNode;
messageId: string | null;
senderId: string;
layout: Layout;
contentTime: number | undefined;
classname?: string;
messageBoxWraperClassname?: string; // message box的直接父亲样式
messageBubbleClassname?: string; // message消息气泡的样式
messageBubbleWrapperClassname?: string; // message消息气泡的父亲样式
messageErrorWrapperClassname?: string; // message错误的父亲样式
isHoverShowUserInfo?: boolean; // hover的时候是否显示用户详细信息
showBackground?: boolean;
extendedUserInfo?: {
userLabel?: UserLabelInfo | null;
userUniqueName?: string;
};
/**
* 容器动态宽度,用于动态计算图片尺寸
*/
imageAutoSizeContainerWidth?: number;
/**
* 是否启用图片自适应模式
*/
enableImageAutoSize?: boolean;
eventCallbacks?: IEventCallbacks;
/**
* 针对 JS Error 的响应
*/
onError?: (error: unknown) => void;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { exhaustiveCheckSimple } from '@coze-common/chat-area-utils';
import { type MessageBoxInnerVariantProps } from '../../../variants/message-box-inner-variants';
import { type MessageBoxTheme } from './type';
export const getMessageBoxInnerVariantsByTheme: (props: {
theme: MessageBoxTheme;
}) => Pick<MessageBoxInnerVariantProps, 'color' | 'border' | 'tight'> = ({
theme,
}) => {
if (theme === 'primary' || theme === 'whiteness' || theme === 'grey') {
return { color: theme, border: null, tight: false };
}
if (theme === 'colorful') {
return { color: 'primary', border: null, tight: false };
}
if (theme === 'border') {
return { color: 'whiteness', border: 'primary', tight: true };
}
if (theme === 'color-border') {
return { color: 'whiteness', border: 'highlight', tight: false };
}
if (theme === 'color-border-card') {
return { color: 'whiteness', border: 'highlight', tight: true };
}
if (theme === 'none') {
return { tight: true, color: null, border: null };
}
exhaustiveCheckSimple(theme);
return { tight: false, color: null, border: null };
};

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cva, type VariantProps } from 'class-variance-authority';
export const messageBoxContainerVariants = cva(['flex', 'flex-row', 'my-0'], {
variants: {
isMobileLayout: {
true: ['mx-[12px]'],
false: ['mx-[24px]'],
},
},
});
export const botNicknameVariants = cva(
[
'text-base',
'font-normal',
'leading-[16px]',
'break-words',
'flex-shrink-0',
'!max-w-[400px]',
],
{
variants: {
showBackground: {
true: ['coz-fg-images-user-name'],
false: ['coz-fg-secondary'],
},
},
},
);
export type BotNicknameVariantsProps = Required<
VariantProps<typeof botNicknameVariants>
>;
export const typeSafeBotNicknameVariants: (
props: BotNicknameVariantsProps,
) => string = botNicknameVariants;

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { formatMessageBoxContentTime } from '../../../utils/date-time';
export const MessageContentTime = ({
contentTime,
className,
showBackground,
}: {
contentTime?: number;
className?: string;
showBackground: boolean;
}) => {
if (!contentTime) {
return null;
}
return (
<span
className={classNames(
'text-[12px] leading-[16px] ml-[8px] font-normal',
'chat-uikit-message-box-container__message-content-time',
{
'coz-fg-images-secondary': showBackground,
'coz-fg-secondary': !showBackground,
},
className,
)}
>
{formatMessageBoxContentTime(contentTime)}
</span>
);
};
MessageContentTime.displayName = 'MessageContentTime';

Some files were not shown because too many files have changed in this diff Show More