mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
feat(webui): Infrastructure Setup (Prerequisites)
This commit is contained in:
parent
ec0586b135
commit
af76450dee
23 changed files with 4367 additions and 374 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -63,3 +63,6 @@ patch_output.log
|
|||
docs-site/.next
|
||||
# content is a symlink to ../docs
|
||||
docs-site/content
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
|
|
|||
522
eslint.config.js
522
eslint.config.js
|
|
@ -1,3 +1,6 @@
|
|||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import storybook from "eslint-plugin-storybook";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
|
|
@ -13,277 +16,260 @@ import importPlugin from 'eslint-plugin-import';
|
|||
import vitest from '@vitest/eslint-plugin';
|
||||
import globals from 'globals';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Global ignores
|
||||
ignores: [
|
||||
'node_modules/*',
|
||||
'packages/**/dist/**',
|
||||
'bundle/**',
|
||||
'package/bundle/**',
|
||||
'.integration-tests/**',
|
||||
'packages/**/.integration-test/**',
|
||||
'dist/**',
|
||||
'docs-site/.next/**',
|
||||
'docs-site/out/**',
|
||||
export default tseslint.config({
|
||||
// Global ignores
|
||||
ignores: [
|
||||
'node_modules/*',
|
||||
'packages/**/dist/**',
|
||||
'bundle/**',
|
||||
'package/bundle/**',
|
||||
'.integration-tests/**',
|
||||
'packages/**/.integration-test/**',
|
||||
'dist/**',
|
||||
'docs-site/.next/**',
|
||||
'docs-site/out/**',
|
||||
],
|
||||
}, eslint.configs.recommended, ...tseslint.configs.recommended, reactHooks.configs['recommended-latest'], reactPlugin.configs.flat.recommended, // Add this if you are using React 17+
|
||||
reactPlugin.configs.flat['jsx-runtime'], {
|
||||
// Settings for eslint-plugin-react
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
// Import specific config
|
||||
files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...importPlugin.configs.recommended.rules,
|
||||
...importPlugin.configs.typescript.rules,
|
||||
'import/no-default-export': 'warn',
|
||||
'import/no-unresolved': 'off', // Disable for now, can be noisy with monorepos/paths
|
||||
},
|
||||
}, {
|
||||
// General overrides and rules for the project (TS/TSX files)
|
||||
files: ['packages/*/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// We use TypeScript for React components; prop-types are unnecessary
|
||||
'react/prop-types': 'off',
|
||||
// General Best Practice Rules (subset adapted for flat config)
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
curly: ['error', 'multi-line'],
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as' },
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{ ignoreParameters: true, ignoreProperties: true },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ disallowTypeAnnotations: false },
|
||||
],
|
||||
'@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'import/no-internal-modules': [
|
||||
'error',
|
||||
{
|
||||
allow: [
|
||||
'react-dom/test-utils',
|
||||
'react-dom/client',
|
||||
'memfs/lib/volume.js',
|
||||
'yargs/**',
|
||||
'msw/node',
|
||||
'**/generated/**',
|
||||
'./styles/tailwind.css',
|
||||
'./styles/App.css',
|
||||
'./styles/style.css'
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/no-relative-packages': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'CallExpression[callee.name="require"]',
|
||||
message: 'Avoid using require(). Use ES6 imports instead.',
|
||||
},
|
||||
{
|
||||
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
|
||||
message:
|
||||
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
|
||||
},
|
||||
],
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unused-expressions': 'off', // Disable base rule
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
// Enable TS version
|
||||
'error',
|
||||
{ allowShortCircuit: true, allowTernary: true },
|
||||
],
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'one-var': ['error', 'never'],
|
||||
'prefer-arrow-callback': 'error',
|
||||
'prefer-const': ['error', { destructuring: 'all' }],
|
||||
radix: 'error',
|
||||
'default-case': 'error',
|
||||
},
|
||||
}, {
|
||||
files: ['packages/*/src/**/*.test.{ts,tsx}', 'packages/**/test/**/*.test.{ts,tsx}'],
|
||||
plugins: {
|
||||
vitest,
|
||||
},
|
||||
rules: {
|
||||
...vitest.configs.recommended.rules,
|
||||
'vitest/expect-expect': 'off',
|
||||
'vitest/no-commented-out-tests': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+
|
||||
{
|
||||
// Settings for eslint-plugin-react
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
}, // extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/*/scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
}, {
|
||||
files: ['packages/vscode-ide-companion/esbuild.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
}, // extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['packages/vscode-ide-companion/scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
}, // extra settings for core package scripts
|
||||
{
|
||||
files: ['packages/core/scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
}, // Prettier config must be last
|
||||
prettierConfig, // extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./integration-tests/**/*.{js,ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
}, // Settings for docs-site directory
|
||||
{
|
||||
files: ['docs-site/**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Import specific config
|
||||
files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...importPlugin.configs.recommended.rules,
|
||||
...importPlugin.configs.typescript.rules,
|
||||
'import/no-default-export': 'warn',
|
||||
'import/no-unresolved': 'off', // Disable for now, can be noisy with monorepos/paths
|
||||
},
|
||||
rules: {
|
||||
// Allow relaxed rules for documentation site
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
},
|
||||
{
|
||||
// General overrides and rules for the project (TS/TSX files)
|
||||
files: ['packages/*/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// We use TypeScript for React components; prop-types are unnecessary
|
||||
'react/prop-types': 'off',
|
||||
// General Best Practice Rules (subset adapted for flat config)
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
curly: ['error', 'multi-line'],
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as' },
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{ ignoreParameters: true, ignoreProperties: true },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ disallowTypeAnnotations: false },
|
||||
],
|
||||
'@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'import/no-internal-modules': [
|
||||
'error',
|
||||
{
|
||||
allow: [
|
||||
'react-dom/test-utils',
|
||||
'react-dom/client',
|
||||
'memfs/lib/volume.js',
|
||||
'yargs/**',
|
||||
'msw/node',
|
||||
'**/generated/**',
|
||||
'./styles/tailwind.css',
|
||||
'./styles/App.css',
|
||||
'./styles/style.css'
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/no-relative-packages': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'CallExpression[callee.name="require"]',
|
||||
message: 'Avoid using require(). Use ES6 imports instead.',
|
||||
},
|
||||
{
|
||||
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
|
||||
message:
|
||||
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
|
||||
},
|
||||
],
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unused-expressions': 'off', // Disable base rule
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
// Enable TS version
|
||||
'error',
|
||||
{ allowShortCircuit: true, allowTernary: true },
|
||||
],
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'one-var': ['error', 'never'],
|
||||
'prefer-arrow-callback': 'error',
|
||||
'prefer-const': ['error', { destructuring: 'all' }],
|
||||
radix: 'error',
|
||||
'default-case': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/*/src/**/*.test.{ts,tsx}', 'packages/**/test/**/*.test.{ts,tsx}'],
|
||||
plugins: {
|
||||
vitest,
|
||||
},
|
||||
rules: {
|
||||
...vitest.configs.recommended.rules,
|
||||
'vitest/expect-expect': 'off',
|
||||
'vitest/no-commented-out-tests': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/*/scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/vscode-ide-companion/esbuild.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
// extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['packages/vscode-ide-companion/scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
// extra settings for core package scripts
|
||||
{
|
||||
files: ['packages/core/scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
// Prettier config must be last
|
||||
prettierConfig,
|
||||
// extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./integration-tests/**/*.{js,ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// Settings for docs-site directory
|
||||
{
|
||||
files: ['docs-site/**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Allow relaxed rules for documentation site
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
}, storybook.configs["flat/recommended"]);
|
||||
|
|
|
|||
2664
package-lock.json
generated
2664
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -152,7 +152,7 @@
|
|||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@qwen-code/webui": "workspace:*",
|
||||
"@qwen-code/webui": "*",
|
||||
"semver": "^7.7.2",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"cors": "^2.8.5",
|
||||
|
|
|
|||
25
packages/webui/.storybook/main.ts
Normal file
25
packages/webui/.storybook/main.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
import { dirname } from 'path';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string): string {
|
||||
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
getAbsolutePath('@chromatic-com/storybook'),
|
||||
getAbsolutePath('@storybook/addon-vitest'),
|
||||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
getAbsolutePath('@storybook/addon-docs'),
|
||||
getAbsolutePath('@storybook/addon-onboarding'),
|
||||
],
|
||||
framework: getAbsolutePath('@storybook/react-vite'),
|
||||
};
|
||||
export default config;
|
||||
11
packages/webui/.storybook/preview.css
Normal file
11
packages/webui/.storybook/preview.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '../src/styles/variables.css';
|
||||
15
packages/webui/.storybook/preview.ts
Normal file
15
packages/webui/.storybook/preview.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { Preview } from '@storybook/react-vite';
|
||||
import './preview.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
428
packages/webui/WEBUI_MIGRATION_PLAN_EN.md
Normal file
428
packages/webui/WEBUI_MIGRATION_PLAN_EN.md
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
# WebUI Component Library Extraction Plan
|
||||
|
||||
## 1. Background and Goals
|
||||
|
||||
### 1.1 Background
|
||||
|
||||
`packages/vscode-ide-companion` is a VSCode extension whose core content is a WebView page with UI components provided by React. As the product line expands, more scenarios require building products with Web UI:
|
||||
|
||||
- **Chrome Browser Extension** - Sidebar chat interface
|
||||
- **Web Chat Page** - Pure web application
|
||||
- **Conversation Share Page** - Render conversations as static HTML
|
||||
|
||||
For excellent software engineering architecture, we need to unify and reuse UI components across products.
|
||||
|
||||
### 1.2 Goals
|
||||
|
||||
1. Extract components from `vscode-ide-companion/src/webview/` into an independent `@qwen-code/webui` package
|
||||
2. Establish a layered architecture: Pure UI components + Business UI components
|
||||
3. Use Vite + Storybook for development and component showcase
|
||||
4. Abstract platform capabilities through Platform Context for cross-platform reuse
|
||||
5. Provide Tailwind CSS preset to ensure UI consistency across products
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State Analysis
|
||||
|
||||
### 2.1 Current Code Structure
|
||||
|
||||
`packages/vscode-ide-companion/src/webview/` contains 77 files:
|
||||
|
||||
```
|
||||
webview/
|
||||
├── App.tsx # Main entry
|
||||
├── components/
|
||||
│ ├── icons/ # 8 icon components
|
||||
│ ├── layout/ # 8 layout components
|
||||
│ │ ├── ChatHeader.tsx
|
||||
│ │ ├── InputForm.tsx
|
||||
│ │ ├── SessionSelector.tsx
|
||||
│ │ ├── EmptyState.tsx
|
||||
│ │ ├── Onboarding.tsx
|
||||
│ │ └── ...
|
||||
│ ├── messages/ # Message display components
|
||||
│ │ ├── UserMessage.tsx
|
||||
│ │ ├── Assistant/
|
||||
│ │ ├── MarkdownRenderer/
|
||||
│ │ ├── ThinkingMessage.tsx
|
||||
│ │ ├── Waiting/
|
||||
│ │ └── toolcalls/ # 16 tool call components
|
||||
│ ├── PermissionDrawer/ # Permission request drawer
|
||||
│ └── Tooltip.tsx
|
||||
├── hooks/ # Custom hooks
|
||||
├── handlers/ # Message handlers
|
||||
├── styles/ # CSS styles
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
### 2.2 Key Dependency Analysis
|
||||
|
||||
**Platform Coupling Points:**
|
||||
|
||||
- `useVSCode` hook - Calls `acquireVsCodeApi()` for message communication
|
||||
- `handlers/` - Handles VSCode message protocol
|
||||
- Some type definitions come from `../types/` directory
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ App.tsx (Entry) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ hooks/ │ handlers/ │ components/ │
|
||||
│ ├─useVSCode ◄───┼──────────────────┼──────────────────┤
|
||||
│ ├─useSession │ ├─MessageRouter │ ├─icons/ │
|
||||
│ ├─useFileContext│ ├─AuthHandler │ ├─layout/ │
|
||||
│ └─... │ └─... │ ├─messages/ │
|
||||
│ │ │ └─PermDrawer/ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ VSCode API (acquireVsCodeApi) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Target Architecture
|
||||
|
||||
### 3.1 Layered Architecture Design
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Layer 3: Platform Adapters │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │VSCode Adapter│ │Chrome Adapter│ │ Web Adapter │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
├─────────┼────────────────┼────────────────┼────────────┤
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Context Provider │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: Chat Components │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ MessageList│ │ ChatHeader │ │ InputForm │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Layer 1: Primitives (Pure UI) │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │ Button │ │ Input │ │ Icons │ │Tooltip │ │
|
||||
│ └────────┘ └────────┘ └────────┘ └────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Platform Context Design
|
||||
|
||||
```typescript
|
||||
// @qwen-code/webui/src/context/PlatformContext.ts
|
||||
interface PlatformContext {
|
||||
// Message communication
|
||||
postMessage: (message: unknown) => void;
|
||||
onMessage: (handler: (message: unknown) => void) => () => void;
|
||||
|
||||
// File operations
|
||||
openFile?: (path: string) => void;
|
||||
attachFile?: () => void;
|
||||
|
||||
// Authentication
|
||||
login?: () => void;
|
||||
|
||||
// Platform info
|
||||
platform: 'vscode' | 'chrome' | 'web' | 'share';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Technical Solution
|
||||
|
||||
### 4.1 Build Configuration (Vite Library Mode)
|
||||
|
||||
**Output formats:**
|
||||
|
||||
- ESM (`dist/index.js`) - Primary format
|
||||
- CJS (`dist/index.cjs`) - Compatibility
|
||||
- TypeScript declarations (`dist/index.d.ts`)
|
||||
|
||||
```javascript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 4.2 Tailwind Preset Solution
|
||||
|
||||
```javascript
|
||||
// @qwen-code/webui/tailwind.preset.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'app-primary': 'var(--app-primary)',
|
||||
'app-background': 'var(--app-primary-background)',
|
||||
'app-foreground': 'var(--app-primary-foreground)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Consumer's tailwind.config.js
|
||||
module.exports = {
|
||||
presets: [require('@qwen-code/webui/tailwind.preset')],
|
||||
content: [
|
||||
'./src/**/*.{ts,tsx}',
|
||||
'./node_modules/@qwen-code/webui/dist/**/*.js',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 Storybook Configuration
|
||||
|
||||
```
|
||||
packages/webui/
|
||||
├── .storybook/
|
||||
│ ├── main.ts # Storybook config
|
||||
│ ├── preview.ts # Global decorators
|
||||
│ └── manager.ts # UI config
|
||||
└── src/
|
||||
└── stories/ # Story files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Component Migration Classification
|
||||
|
||||
### 5.1 Batch 1: No-dependency Components (Ready to migrate)
|
||||
|
||||
| Component | Source Path | Complexity | Notes |
|
||||
| ------------------ | ------------------------ | ---------- | --------------------------- |
|
||||
| Icons | `components/icons/` | Low | 8 icon components, pure SVG |
|
||||
| Tooltip | `components/Tooltip.tsx` | Low | Pure UI |
|
||||
| WaitingMessage | `messages/Waiting/` | Low | Loading state display |
|
||||
| InterruptedMessage | `messages/Waiting/` | Low | Interrupted state display |
|
||||
|
||||
### 5.2 Batch 2: Light-dependency Components (Need props abstraction)
|
||||
|
||||
| Component | Source Path | Dependency | Refactoring |
|
||||
| ---------------- | ------------------------------ | ----------- | ---------------- |
|
||||
| UserMessage | `messages/UserMessage.tsx` | onFileClick | Props injection |
|
||||
| AssistantMessage | `messages/Assistant/` | onFileClick | Props injection |
|
||||
| ThinkingMessage | `messages/ThinkingMessage.tsx` | onFileClick | Props injection |
|
||||
| MarkdownRenderer | `messages/MarkdownRenderer/` | None | Direct migration |
|
||||
| EmptyState | `layout/EmptyState.tsx` | None | Direct migration |
|
||||
| ChatHeader | `layout/ChatHeader.tsx` | callbacks | Props injection |
|
||||
|
||||
### 5.3 Batch 3: Medium-dependency Components (Need Context)
|
||||
|
||||
| Component | Source Path | Dependency | Refactoring |
|
||||
| ------------------- | ---------------------------- | --------------------- | ----------------- |
|
||||
| InputForm | `layout/InputForm.tsx` | Multiple callbacks | Context + Props |
|
||||
| SessionSelector | `layout/SessionSelector.tsx` | session data | Props injection |
|
||||
| CompletionMenu | `layout/CompletionMenu.tsx` | items data | Props injection |
|
||||
| PermissionDrawer | `PermissionDrawer/` | callbacks | Context + Props |
|
||||
| ToolCall components | `messages/toolcalls/` | Various tool displays | Modular migration |
|
||||
|
||||
### 5.4 Batch 4: Heavy-dependency (Keep in platform package)
|
||||
|
||||
| Component/Module | Notes |
|
||||
| ---------------- | ------------------------------------------------- |
|
||||
| App.tsx | Main entry, contains business orchestration logic |
|
||||
| hooks/ | Most require platform adaptation |
|
||||
| handlers/ | VSCode message handling |
|
||||
| Onboarding | Authentication related, platform-specific |
|
||||
|
||||
---
|
||||
|
||||
## 6. Incremental Migration Strategy
|
||||
|
||||
### 6.1 Migration Principles
|
||||
|
||||
1. **Bidirectional compatibility**: During migration, vscode-ide-companion can import from both webui and local
|
||||
2. **One-by-one replacement**: For each migrated component, replace import path in VSCode extension and verify
|
||||
3. **No breaking changes**: Ensure the extension builds and runs normally after each migration
|
||||
|
||||
### 6.2 Migration Workflow
|
||||
|
||||
```
|
||||
Developer ──► @qwen-code/webui ──► vscode-ide-companion
|
||||
│ │ │
|
||||
│ 1. Copy component to webui │
|
||||
│ 2. Add Story for verification │
|
||||
│ 3. Export from index.ts │
|
||||
│ │ │
|
||||
│ └──────────────────────┤
|
||||
│ │
|
||||
│ 4. Update import path
|
||||
│ 5. Delete original component
|
||||
│ 6. Build and test
|
||||
```
|
||||
|
||||
### 6.3 Example: Migrating Icons
|
||||
|
||||
```typescript
|
||||
// Before: vscode-ide-companion/src/webview/components/icons/index.ts
|
||||
export { FileIcon } from './FileIcons.js';
|
||||
|
||||
// After: Update import
|
||||
import { FileIcon } from '@qwen-code/webui';
|
||||
// or import { FileIcon } from '@qwen-code/webui/icons';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Task Breakdown
|
||||
|
||||
### Phase 0: Infrastructure Setup (Prerequisites)
|
||||
|
||||
- [ ] **T0-1**: Vite build configuration
|
||||
- [ ] **T0-2**: Storybook configuration
|
||||
- [ ] **T0-3**: Tailwind preset creation
|
||||
- [ ] **T0-4**: Platform Context definition
|
||||
- [ ] **T0-5**: Shared types migration
|
||||
|
||||
### Phase 1: Pure UI Components Migration
|
||||
|
||||
- [ ] **T1-1**: Icons components migration (8 files)
|
||||
- [ ] **T1-2**: Tooltip component migration
|
||||
- [ ] **T1-3**: WaitingMessage / InterruptedMessage migration
|
||||
- [ ] **T1-4**: Basic Button/Input components refinement
|
||||
|
||||
### Phase 2: Message Components Migration
|
||||
|
||||
- [ ] **T2-1**: MarkdownRenderer migration
|
||||
- [ ] **T2-2**: UserMessage migration
|
||||
- [ ] **T2-3**: AssistantMessage migration
|
||||
- [ ] **T2-4**: ThinkingMessage migration
|
||||
|
||||
### Phase 3: Layout Components Migration
|
||||
|
||||
- [ ] **T3-1**: ChatHeader migration
|
||||
- [ ] **T3-2**: EmptyState migration
|
||||
- [ ] **T3-3**: InputForm migration (requires Context)
|
||||
- [ ] **T3-4**: SessionSelector migration
|
||||
- [ ] **T3-5**: CompletionMenu migration
|
||||
|
||||
### Phase 4: Complex Components Migration
|
||||
|
||||
- [ ] **T4-1**: PermissionDrawer migration
|
||||
- [ ] **T4-2**: ToolCall series components migration (16 files)
|
||||
|
||||
### Phase 5: Platform Adapters
|
||||
|
||||
- [ ] **T5-1**: VSCode Adapter implementation
|
||||
- [ ] **T5-2**: Chrome Extension Adapter
|
||||
- [ ] **T5-3**: Web/Share Page Adapter
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks and Considerations
|
||||
|
||||
### 8.1 Common Pitfalls
|
||||
|
||||
1. **Tailwind Class Name Tree Shaking**
|
||||
- Problem: Tailwind class names may be removed after library bundling
|
||||
- Solution: Consumer's `content` config needs to include `node_modules/@qwen-code/webui`
|
||||
|
||||
2. **CSS Variable Scope**
|
||||
- Problem: Variables like `var(--app-primary)` need to be defined by consumers
|
||||
- Solution: Provide default CSS variables file, or define fallbacks in Tailwind preset
|
||||
|
||||
3. **React Version Compatibility**
|
||||
- Current vscode-ide-companion uses React 19, webui's peerDependencies is React 18
|
||||
- Need to update peerDependencies to `"react": "^18.0.0 || ^19.0.0"`
|
||||
|
||||
4. **ESM/CJS Compatibility**
|
||||
- VSCode extensions may require CJS format
|
||||
- Vite needs to be configured for dual format output
|
||||
|
||||
### 8.2 Industry References
|
||||
|
||||
- **Radix UI**: Pure Headless components, styles completely controlled by consumers
|
||||
- **shadcn/ui**: Copy components into project, rather than importing as dependency
|
||||
- **Ant Design**: Complete component library, customization through ConfigProvider
|
||||
|
||||
### 8.3 Acceptance Criteria
|
||||
|
||||
Each migration task completion requires:
|
||||
|
||||
1. Component has corresponding Storybook Story
|
||||
2. Import in vscode-ide-companion has been updated
|
||||
3. Extension builds successfully (`npm run build:vscode`)
|
||||
4. Extension functionality works (manual testing or existing tests pass)
|
||||
|
||||
---
|
||||
|
||||
## 9. Time Estimation
|
||||
|
||||
| Phase | Tasks | Estimated Days | Parallelizable |
|
||||
| ------- | ----- | -------------- | -------------- |
|
||||
| Phase 0 | 5 | 2-3 days | Partially |
|
||||
| Phase 1 | 4 | 1-2 days | Fully |
|
||||
| Phase 2 | 4 | 2-3 days | Fully |
|
||||
| Phase 3 | 5 | 3-4 days | Partially |
|
||||
| Phase 4 | 2 | 3-4 days | Yes |
|
||||
| Phase 5 | 3 | 2-3 days | Yes |
|
||||
|
||||
**Total**: Approximately 13-19 person-days (sequential execution), can be reduced to 1-2 weeks with parallel work
|
||||
|
||||
---
|
||||
|
||||
## 10. Development and Debugging Workflow
|
||||
|
||||
### 10.1 Component Development Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Development Workflow │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Develop/Modify Component │
|
||||
│ └── Edit files in @qwen-code/webui/src/ │
|
||||
│ │
|
||||
│ 2. Debug with Storybook │
|
||||
│ └── npm run storybook (port 6006) │
|
||||
│ └── View component in isolation │
|
||||
│ └── Test different props/states │
|
||||
│ │
|
||||
│ 3. Build Library │
|
||||
│ └── npm run build │
|
||||
│ └── Outputs: dist/index.js, dist/index.cjs, dist/index.d.ts │
|
||||
│ │
|
||||
│ 4. Use in VSCode Extension │
|
||||
│ └── import { Component } from '@qwen-code/webui' │
|
||||
│ └── No UI code modifications in vscode-ide-companion │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 10.2 Debugging Commands
|
||||
|
||||
```bash
|
||||
# Start Storybook for component development
|
||||
cd packages/webui
|
||||
npm run storybook
|
||||
|
||||
# Watch mode for library development
|
||||
npm run dev
|
||||
|
||||
# Build library for production
|
||||
npm run build
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### 10.3 Key Principles
|
||||
|
||||
1. **Single Source of Truth**: All UI components live in `@qwen-code/webui`
|
||||
2. **Storybook First**: Debug and validate components in Storybook before integration
|
||||
3. **No UI Code in Consumers**: `vscode-ide-companion` only imports and uses components
|
||||
4. **Platform Abstraction**: Use `PlatformContext` for platform-specific behaviors
|
||||
428
packages/webui/WEBUI_MIGRATION_PLAN_ZH.md
Normal file
428
packages/webui/WEBUI_MIGRATION_PLAN_ZH.md
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
# WebUI 组件库抽离计划
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
`packages/vscode-ide-companion` 是一个 VSCode 插件,其核心内容是一个 WebView 页面,大量 UI 部分由 React 组件提供。随着产品线扩展,越来越多的场景需要构建包含 Web UI 的产品:
|
||||
|
||||
- **Chrome 浏览器扩展** - 侧边栏聊天界面
|
||||
- **Web 端聊天页面** - 纯 Web 应用
|
||||
- **对话分享页面** - 将对话渲染为静态 HTML
|
||||
|
||||
对于优秀的软件工程架构,我们需要让 UI 做到统一且可复用。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
1. 将 `vscode-ide-companion/src/webview/` 中的组件抽离到独立的 `@qwen-code/webui` 包
|
||||
2. 建立分层架构:纯 UI 组件 + 业务 UI 组件
|
||||
3. 使用 Vite + Storybook 进行开发和组件展示
|
||||
4. 通过 Platform Context 抽象平台能力,实现跨平台复用
|
||||
5. 提供 Tailwind CSS 预设,保证多产品 UI 一致性
|
||||
|
||||
---
|
||||
|
||||
## 二、现状分析
|
||||
|
||||
### 2.1 当前代码结构
|
||||
|
||||
`packages/vscode-ide-companion/src/webview/` 包含 77 个文件:
|
||||
|
||||
```
|
||||
webview/
|
||||
├── App.tsx # 主入口
|
||||
├── components/
|
||||
│ ├── icons/ # 8 个图标组件
|
||||
│ ├── layout/ # 8 个布局组件
|
||||
│ │ ├── ChatHeader.tsx
|
||||
│ │ ├── InputForm.tsx
|
||||
│ │ ├── SessionSelector.tsx
|
||||
│ │ ├── EmptyState.tsx
|
||||
│ │ ├── Onboarding.tsx
|
||||
│ │ └── ...
|
||||
│ ├── messages/ # 消息展示组件
|
||||
│ │ ├── UserMessage.tsx
|
||||
│ │ ├── Assistant/
|
||||
│ │ ├── MarkdownRenderer/
|
||||
│ │ ├── ThinkingMessage.tsx
|
||||
│ │ ├── Waiting/
|
||||
│ │ └── toolcalls/ # 16 个工具调用组件
|
||||
│ ├── PermissionDrawer/ # 权限请求抽屉
|
||||
│ └── Tooltip.tsx
|
||||
├── hooks/ # 自定义 hooks
|
||||
├── handlers/ # 消息处理器
|
||||
├── styles/ # CSS 样式
|
||||
└── utils/ # 工具函数
|
||||
```
|
||||
|
||||
### 2.2 关键依赖分析
|
||||
|
||||
**平台耦合点:**
|
||||
|
||||
- `useVSCode` hook - 调用 `acquireVsCodeApi()` 进行消息通信
|
||||
- `handlers/` - 处理 VSCode 消息协议
|
||||
- 部分类型定义来自 `../types/` 目录
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ App.tsx (入口) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ hooks/ │ handlers/ │ components/ │
|
||||
│ ├─useVSCode ◄───┼──────────────────┼──────────────────┤
|
||||
│ ├─useSession │ ├─MessageRouter │ ├─icons/ │
|
||||
│ ├─useFileContext│ ├─AuthHandler │ ├─layout/ │
|
||||
│ └─... │ └─... │ ├─messages/ │
|
||||
│ │ │ └─PermDrawer/ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ VSCode API (acquireVsCodeApi) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、目标架构
|
||||
|
||||
### 3.1 分层架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Layer 3: Platform Adapters │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │VSCode Adapter│ │Chrome Adapter│ │ Web Adapter │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
├─────────┼────────────────┼────────────────┼────────────┤
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Context Provider │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: Chat Components │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ MessageList│ │ ChatHeader │ │ InputForm │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Layer 1: Primitives (纯 UI) │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │ Button │ │ Input │ │ Icons │ │Tooltip │ │
|
||||
│ └────────┘ └────────┘ └────────┘ └────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Platform Context 设计
|
||||
|
||||
```typescript
|
||||
// @qwen-code/webui/src/context/PlatformContext.ts
|
||||
interface PlatformContext {
|
||||
// 消息通信
|
||||
postMessage: (message: unknown) => void;
|
||||
onMessage: (handler: (message: unknown) => void) => () => void;
|
||||
|
||||
// 文件操作
|
||||
openFile?: (path: string) => void;
|
||||
attachFile?: () => void;
|
||||
|
||||
// 认证
|
||||
login?: () => void;
|
||||
|
||||
// 平台信息
|
||||
platform: 'vscode' | 'chrome' | 'web' | 'share';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、技术方案
|
||||
|
||||
### 4.1 构建配置(Vite Library Mode)
|
||||
|
||||
**输出格式:**
|
||||
|
||||
- ESM (`dist/index.js`) - 主要格式
|
||||
- CJS (`dist/index.cjs`) - 兼容性
|
||||
- TypeScript 声明 (`dist/index.d.ts`)
|
||||
|
||||
```javascript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 4.2 Tailwind 预设方案
|
||||
|
||||
```javascript
|
||||
// @qwen-code/webui/tailwind.preset.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'app-primary': 'var(--app-primary)',
|
||||
'app-background': 'var(--app-primary-background)',
|
||||
'app-foreground': 'var(--app-primary-foreground)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 消费方 tailwind.config.js
|
||||
module.exports = {
|
||||
presets: [require('@qwen-code/webui/tailwind.preset')],
|
||||
content: [
|
||||
'./src/**/*.{ts,tsx}',
|
||||
'./node_modules/@qwen-code/webui/dist/**/*.js',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 Storybook 配置
|
||||
|
||||
```
|
||||
packages/webui/
|
||||
├── .storybook/
|
||||
│ ├── main.ts # Storybook 配置
|
||||
│ ├── preview.ts # 全局装饰器
|
||||
│ └── manager.ts # UI 配置
|
||||
└── src/
|
||||
└── stories/ # Story 文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、组件迁移分类
|
||||
|
||||
### 5.1 第一批:无依赖组件(可立即迁移)
|
||||
|
||||
| 组件 | 来源路径 | 复杂度 | 说明 |
|
||||
| ------------------ | ------------------------ | ------ | -------------------- |
|
||||
| Icons | `components/icons/` | 低 | 8 个图标组件,纯 SVG |
|
||||
| Tooltip | `components/Tooltip.tsx` | 低 | 纯 UI |
|
||||
| WaitingMessage | `messages/Waiting/` | 低 | 加载状态展示 |
|
||||
| InterruptedMessage | `messages/Waiting/` | 低 | 中断状态展示 |
|
||||
|
||||
### 5.2 第二批:轻度依赖组件(需要抽象 props)
|
||||
|
||||
| 组件 | 来源路径 | 依赖 | 改造方式 |
|
||||
| ---------------- | ------------------------------ | ----------- | --------------- |
|
||||
| UserMessage | `messages/UserMessage.tsx` | onFileClick | 通过 props 注入 |
|
||||
| AssistantMessage | `messages/Assistant/` | onFileClick | 通过 props 注入 |
|
||||
| ThinkingMessage | `messages/ThinkingMessage.tsx` | onFileClick | 通过 props 注入 |
|
||||
| MarkdownRenderer | `messages/MarkdownRenderer/` | 无 | 直接迁移 |
|
||||
| EmptyState | `layout/EmptyState.tsx` | 无 | 直接迁移 |
|
||||
| ChatHeader | `layout/ChatHeader.tsx` | callbacks | 通过 props 注入 |
|
||||
|
||||
### 5.3 第三批:中度依赖组件(需要 Context)
|
||||
|
||||
| 组件 | 来源路径 | 依赖 | 改造方式 |
|
||||
| ---------------- | ---------------------------- | -------------- | --------------- |
|
||||
| InputForm | `layout/InputForm.tsx` | 多个 callbacks | Context + Props |
|
||||
| SessionSelector | `layout/SessionSelector.tsx` | session 数据 | Props 注入 |
|
||||
| CompletionMenu | `layout/CompletionMenu.tsx` | items 数据 | Props 注入 |
|
||||
| PermissionDrawer | `PermissionDrawer/` | 回调函数 | Context + Props |
|
||||
| ToolCall 组件 | `messages/toolcalls/` | 多种工具展示 | 分模块迁移 |
|
||||
|
||||
### 5.4 第四批:重度依赖(保留在平台包)
|
||||
|
||||
| 组件/模块 | 说明 |
|
||||
| ---------- | ------------------------ |
|
||||
| App.tsx | 总入口,包含业务编排逻辑 |
|
||||
| hooks/ | 大部分需要平台适配 |
|
||||
| handlers/ | VSCode 消息处理 |
|
||||
| Onboarding | 认证相关,平台特定 |
|
||||
|
||||
---
|
||||
|
||||
## 六、渐进式迁移策略
|
||||
|
||||
### 6.1 迁移原则
|
||||
|
||||
1. **双向兼容**:迁移期间,vscode-ide-companion 可以同时从 webui 和本地导入
|
||||
2. **逐个替换**:每迁移一个组件,在 VSCode 插件中替换导入路径并验证
|
||||
3. **不破坏现有功能**:确保每次迁移后插件可正常构建和运行
|
||||
|
||||
### 6.2 迁移流程
|
||||
|
||||
```
|
||||
开发者 ──► @qwen-code/webui ──► vscode-ide-companion
|
||||
│ │ │
|
||||
│ 1. 复制组件到 webui │
|
||||
│ 2. 添加 Story 验证 │
|
||||
│ 3. 从 index.ts 导出 │
|
||||
│ │ │
|
||||
│ └──────────────────────┤
|
||||
│ │
|
||||
│ 4. 更新 import 路径
|
||||
│ 5. 删除原组件文件
|
||||
│ 6. 构建测试验证
|
||||
```
|
||||
|
||||
### 6.3 示例:迁移 Icons
|
||||
|
||||
```typescript
|
||||
// Before: vscode-ide-companion/src/webview/components/icons/index.ts
|
||||
export { FileIcon } from './FileIcons.js';
|
||||
|
||||
// After: 修改导入
|
||||
import { FileIcon } from '@qwen-code/webui';
|
||||
// 或 import { FileIcon } from '@qwen-code/webui/icons';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、任务拆分
|
||||
|
||||
### Phase 0: 基础设施搭建(前置任务)
|
||||
|
||||
- [ ] **T0-1**: Vite 构建配置
|
||||
- [ ] **T0-2**: Storybook 配置
|
||||
- [ ] **T0-3**: Tailwind 预设创建
|
||||
- [ ] **T0-4**: Platform Context 定义
|
||||
- [ ] **T0-5**: 类型定义迁移(共享 types)
|
||||
|
||||
### Phase 1: 纯 UI 组件迁移
|
||||
|
||||
- [ ] **T1-1**: Icons 组件迁移(8 个文件)
|
||||
- [ ] **T1-2**: Tooltip 组件迁移
|
||||
- [ ] **T1-3**: WaitingMessage / InterruptedMessage 迁移
|
||||
- [ ] **T1-4**: 基础 Button/Input 组件完善
|
||||
|
||||
### Phase 2: 消息组件迁移
|
||||
|
||||
- [ ] **T2-1**: MarkdownRenderer 迁移
|
||||
- [ ] **T2-2**: UserMessage 迁移
|
||||
- [ ] **T2-3**: AssistantMessage 迁移
|
||||
- [ ] **T2-4**: ThinkingMessage 迁移
|
||||
|
||||
### Phase 3: 布局组件迁移
|
||||
|
||||
- [ ] **T3-1**: ChatHeader 迁移
|
||||
- [ ] **T3-2**: EmptyState 迁移
|
||||
- [ ] **T3-3**: InputForm 迁移(需要 Context)
|
||||
- [ ] **T3-4**: SessionSelector 迁移
|
||||
- [ ] **T3-5**: CompletionMenu 迁移
|
||||
|
||||
### Phase 4: 复杂组件迁移
|
||||
|
||||
- [ ] **T4-1**: PermissionDrawer 迁移
|
||||
- [ ] **T4-2**: ToolCall 系列组件迁移(16 个文件)
|
||||
|
||||
### Phase 5: 平台适配器
|
||||
|
||||
- [ ] **T5-1**: VSCode Adapter 实现
|
||||
- [ ] **T5-2**: Chrome Extension Adapter
|
||||
- [ ] **T5-3**: Web/Share Page Adapter
|
||||
|
||||
---
|
||||
|
||||
## 八、风险与注意事项
|
||||
|
||||
### 8.1 常见坑点
|
||||
|
||||
1. **Tailwind 类名 Tree Shaking**
|
||||
- 问题:组件库打包后 Tailwind 类名可能被移除
|
||||
- 解决:消费方的 `content` 配置需要包含 `node_modules/@qwen-code/webui`
|
||||
|
||||
2. **CSS 变量作用域**
|
||||
- 问题:`var(--app-primary)` 等变量需要在消费方定义
|
||||
- 解决:提供默认 CSS 变量文件,或在 Tailwind 预设中定义 fallback
|
||||
|
||||
3. **React 版本兼容**
|
||||
- 当前 vscode-ide-companion 使用 React 19,webui 的 peerDependencies 是 React 18
|
||||
- 需要更新 peerDependencies 为 `"react": "^18.0.0 || ^19.0.0"`
|
||||
|
||||
4. **ESM/CJS 兼容**
|
||||
- VSCode 扩展可能需要 CJS 格式
|
||||
- Vite 需要配置双格式输出
|
||||
|
||||
### 8.2 业界参考
|
||||
|
||||
- **Radix UI**: 纯 Headless 组件,样式完全由消费方控制
|
||||
- **shadcn/ui**: 复制组件到项目中,而非作为依赖引入
|
||||
- **Ant Design**: 完整的组件库,通过 ConfigProvider 进行定制
|
||||
|
||||
### 8.3 验收标准
|
||||
|
||||
每个迁移任务完成后需要:
|
||||
|
||||
1. 组件有对应的 Storybook Story
|
||||
2. vscode-ide-companion 中的导入已更新
|
||||
3. 插件可正常构建 (`npm run build:vscode`)
|
||||
4. 插件功能正常(手动测试或已有测试通过)
|
||||
|
||||
---
|
||||
|
||||
## 九、预估时间
|
||||
|
||||
| 阶段 | 任务数 | 预估人天 | 可并行 |
|
||||
| ------- | ------ | -------- | ---------- |
|
||||
| Phase 0 | 5 | 2-3 天 | 部分可并行 |
|
||||
| Phase 1 | 4 | 1-2 天 | 全部可并行 |
|
||||
| Phase 2 | 4 | 2-3 天 | 全部可并行 |
|
||||
| Phase 3 | 5 | 3-4 天 | 部分可并行 |
|
||||
| Phase 4 | 2 | 3-4 天 | 可并行 |
|
||||
| Phase 5 | 3 | 2-3 天 | 可并行 |
|
||||
|
||||
**总计**:约 13-19 人天(单人顺序执行),如果多人并行可缩短至 1-2 周
|
||||
|
||||
---
|
||||
|
||||
## 十、开发与调试流程
|
||||
|
||||
### 10.1 组件开发流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 开发工作流程 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 开发/修改组件 │
|
||||
│ └── 在 @qwen-code/webui/src/ 中编辑文件 │
|
||||
│ │
|
||||
│ 2. 使用 Storybook 调试 │
|
||||
│ └── npm run storybook (端口 6006) │
|
||||
│ └── 独立查看组件 │
|
||||
│ └── 测试不同的 props/状态 │
|
||||
│ │
|
||||
│ 3. 构建组件库 │
|
||||
│ └── npm run build │
|
||||
│ └── 输出: dist/index.js, dist/index.cjs, dist/index.d.ts │
|
||||
│ │
|
||||
│ 4. 在 VSCode 插件中使用 │
|
||||
│ └── import { Component } from '@qwen-code/webui' │
|
||||
│ └── vscode-ide-companion 中不再修改 UI 代码 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 10.2 调试命令
|
||||
|
||||
```bash
|
||||
# 启动 Storybook 进行组件开发
|
||||
cd packages/webui
|
||||
npm run storybook
|
||||
|
||||
# 监听模式进行库开发
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 类型检查
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### 10.3 核心原则
|
||||
|
||||
1. **单一数据源**: 所有 UI 组件都在 `@qwen-code/webui` 中
|
||||
2. **Storybook 优先**: 在集成前先在 Storybook 中调试和验证组件
|
||||
3. **消费方不修改 UI 代码**: `vscode-ide-companion` 只导入和使用组件
|
||||
4. **平台抽象**: 使用 `PlatformContext` 处理平台特定行为
|
||||
|
|
@ -2,28 +2,60 @@
|
|||
"name": "@qwen-code/webui",
|
||||
"version": "0.1.0",
|
||||
"description": "Shared UI components for Qwen Code packages",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./tailwind.preset": "./tailwind.preset.cjs",
|
||||
"./styles.css": "./dist/styles.css"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"tailwind.preset.cjs"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && rollup -c",
|
||||
"dev": "tsc --watch",
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.0.0",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"rollup": "^4.0.0",
|
||||
"rollup-plugin-dts": "^6.0.0"
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.7.0",
|
||||
"storybook": "^10.1.11",
|
||||
"@storybook/react-vite": "^10.1.11",
|
||||
"@chromatic-com/storybook": "^5.0.0",
|
||||
"@storybook/addon-vitest": "^10.1.11",
|
||||
"@storybook/addon-a11y": "^10.1.11",
|
||||
"@storybook/addon-docs": "^10.1.11",
|
||||
"@storybook/addon-onboarding": "^10.1.11",
|
||||
"eslint-plugin-storybook": "^10.1.11",
|
||||
"playwright": "^1.57.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^3.2.4"
|
||||
},
|
||||
"keywords": [
|
||||
"qwen",
|
||||
|
|
|
|||
13
packages/webui/postcss.config.cjs
Normal file
13
packages/webui/postcss.config.cjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import typescript from '@rollup/plugin-typescript';
|
||||
import { dts } from 'rollup-plugin-dts';
|
||||
import pkg from './package.json' with { type: 'json' };
|
||||
|
||||
const name = pkg.name;
|
||||
|
||||
export default [
|
||||
// Browser-friendly version
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
name,
|
||||
file: 'dist/index.min.js',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
},
|
||||
},
|
||||
external: ['react', 'react-dom'],
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
}),
|
||||
],
|
||||
},
|
||||
// ES module version
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{ file: 'dist/index.esm.js', format: 'es' },
|
||||
{ file: 'dist/index.cjs.js', format: 'cjs' },
|
||||
],
|
||||
external: ['react', 'react-dom'],
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
}),
|
||||
],
|
||||
},
|
||||
// Type declarations
|
||||
{
|
||||
input: 'dist/dts/src/index.d.ts',
|
||||
output: [{ file: 'dist/index.d.ts', format: 'es' }],
|
||||
plugins: [dts()],
|
||||
},
|
||||
];
|
||||
83
packages/webui/src/components/ui/Button.stories.tsx
Normal file
83
packages/webui/src/components/ui/Button.stories.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import Button from './Button';
|
||||
|
||||
/**
|
||||
* Button component for user interactions.
|
||||
* Supports multiple variants and sizes.
|
||||
*/
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'UI/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'danger'],
|
||||
description: 'Visual style variant',
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg'],
|
||||
description: 'Button size',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disabled state',
|
||||
},
|
||||
onClick: { action: 'clicked' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: 'Primary Button',
|
||||
variant: 'primary',
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: 'Secondary Button',
|
||||
variant: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: 'Danger Button',
|
||||
variant: 'danger',
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
children: 'Small Button',
|
||||
size: 'sm',
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: 'Large Button',
|
||||
size: 'lg',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: 'Disabled Button',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
68
packages/webui/src/components/ui/Tooltip.stories.tsx
Normal file
68
packages/webui/src/components/ui/Tooltip.stories.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import Tooltip from './Tooltip';
|
||||
import Button from './Button';
|
||||
|
||||
/**
|
||||
* Tooltip component for displaying contextual information on hover.
|
||||
* Supports four positions: top, right, bottom, left.
|
||||
*/
|
||||
const meta: Meta<typeof Tooltip> = {
|
||||
title: 'UI/Tooltip',
|
||||
component: Tooltip,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
position: {
|
||||
control: 'select',
|
||||
options: ['top', 'right', 'bottom', 'left'],
|
||||
description: 'Tooltip position relative to trigger',
|
||||
},
|
||||
content: {
|
||||
control: 'text',
|
||||
description: 'Tooltip content text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Top: Story = {
|
||||
args: {
|
||||
content: 'Tooltip on top',
|
||||
position: 'top',
|
||||
children: <Button>Hover me</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const Right: Story = {
|
||||
args: {
|
||||
content: 'Tooltip on right',
|
||||
position: 'right',
|
||||
children: <Button>Hover me</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const Bottom: Story = {
|
||||
args: {
|
||||
content: 'Tooltip on bottom',
|
||||
position: 'bottom',
|
||||
children: <Button>Hover me</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const Left: Story = {
|
||||
args: {
|
||||
content: 'Tooltip on left',
|
||||
position: 'left',
|
||||
children: <Button>Hover me</Button>,
|
||||
},
|
||||
};
|
||||
89
packages/webui/src/context/PlatformContext.tsx
Normal file
89
packages/webui/src/context/PlatformContext.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
/**
|
||||
* Platform types supported by the webui library
|
||||
*/
|
||||
export type PlatformType = 'vscode' | 'chrome' | 'web' | 'share';
|
||||
|
||||
/**
|
||||
* Platform context interface for cross-platform component reuse.
|
||||
* Each platform adapter implements this interface.
|
||||
*/
|
||||
export interface PlatformContextValue {
|
||||
/** Current platform identifier */
|
||||
platform: PlatformType;
|
||||
|
||||
/** Send message to platform host */
|
||||
postMessage: (message: unknown) => void;
|
||||
|
||||
/** Subscribe to messages from platform host */
|
||||
onMessage: (handler: (message: unknown) => void) => () => void;
|
||||
|
||||
/** Open a file in the platform's editor (optional) */
|
||||
openFile?: (path: string) => void;
|
||||
|
||||
/** Trigger file attachment dialog (optional) */
|
||||
attachFile?: () => void;
|
||||
|
||||
/** Trigger platform login flow (optional) */
|
||||
login?: () => void;
|
||||
|
||||
/** Copy text to clipboard */
|
||||
copyToClipboard?: (text: string) => Promise<void>;
|
||||
|
||||
/** Platform-specific feature flags */
|
||||
features?: {
|
||||
canOpenFile?: boolean;
|
||||
canAttachFile?: boolean;
|
||||
canLogin?: boolean;
|
||||
canCopy?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default noop implementation for platforms without message support
|
||||
*/
|
||||
const defaultContext: PlatformContextValue = {
|
||||
platform: 'web',
|
||||
postMessage: () => {},
|
||||
onMessage: () => () => {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Platform context for accessing platform-specific capabilities
|
||||
*/
|
||||
export const PlatformContext =
|
||||
createContext<PlatformContextValue>(defaultContext);
|
||||
|
||||
/**
|
||||
* Hook to access platform context
|
||||
*/
|
||||
export function usePlatform(): PlatformContextValue {
|
||||
return useContext(PlatformContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider component props
|
||||
*/
|
||||
export interface PlatformProviderProps {
|
||||
children: React.ReactNode;
|
||||
value: PlatformContextValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform context provider component
|
||||
*/
|
||||
export function PlatformProvider({ children, value }: PlatformProviderProps) {
|
||||
return (
|
||||
<PlatformContext.Provider value={value}>
|
||||
{children}
|
||||
</PlatformContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,18 @@
|
|||
// Shared UI Components Export
|
||||
// Export all shared components from this package
|
||||
|
||||
// Context
|
||||
export {
|
||||
PlatformContext,
|
||||
PlatformProvider,
|
||||
usePlatform,
|
||||
} from './context/PlatformContext';
|
||||
export type {
|
||||
PlatformContextValue,
|
||||
PlatformProviderProps,
|
||||
PlatformType,
|
||||
} from './context/PlatformContext';
|
||||
|
||||
// Layout components
|
||||
export { default as Container } from './components/layout/Container';
|
||||
export { default as Header } from './components/layout/Header';
|
||||
|
|
@ -33,3 +45,10 @@ export { useLocalStorage } from './hooks/useLocalStorage';
|
|||
// Types
|
||||
export type { Theme } from './types/theme';
|
||||
export type { MessageProps } from './types/messages';
|
||||
export type { ChatMessage, MessageRole, PlanEntry } from './types/chat';
|
||||
export type {
|
||||
ToolCallStatus,
|
||||
ToolCallLocation,
|
||||
ToolCallContentItem,
|
||||
ToolCallUpdate,
|
||||
} from './types/toolCall';
|
||||
|
|
|
|||
52
packages/webui/src/styles/variables.css
Normal file
52
packages/webui/src/styles/variables.css
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default CSS variables for @qwen-code/webui
|
||||
* Consumers can override these variables to customize the theme.
|
||||
*/
|
||||
:root {
|
||||
/* Primary colors */
|
||||
--app-primary: #3b82f6;
|
||||
--app-primary-hover: #2563eb;
|
||||
--app-primary-foreground: #ffffff;
|
||||
|
||||
/* Background colors */
|
||||
--app-background: #ffffff;
|
||||
--app-background-secondary: #f3f4f6;
|
||||
--app-background-tertiary: #e5e7eb;
|
||||
|
||||
/* Foreground/text colors */
|
||||
--app-foreground: #111827;
|
||||
--app-foreground-secondary: #6b7280;
|
||||
--app-foreground-muted: #9ca3af;
|
||||
|
||||
/* Border colors */
|
||||
--app-border: #e5e7eb;
|
||||
--app-border-focus: #3b82f6;
|
||||
|
||||
/* Status colors */
|
||||
--app-success: #10b981;
|
||||
--app-warning: #f59e0b;
|
||||
--app-error: #ef4444;
|
||||
--app-info: #3b82f6;
|
||||
|
||||
/* Typography */
|
||||
--app-font-sans: system-ui, -apple-system, sans-serif;
|
||||
--app-font-mono: ui-monospace, monospace;
|
||||
|
||||
/* Border radius */
|
||||
--app-radius-sm: 0.25rem;
|
||||
--app-radius-md: 0.375rem;
|
||||
--app-radius-lg: 0.5rem;
|
||||
|
||||
/* Spacing */
|
||||
--app-spacing-xs: 0.25rem;
|
||||
--app-spacing-sm: 0.5rem;
|
||||
--app-spacing-md: 1rem;
|
||||
--app-spacing-lg: 1.5rem;
|
||||
--app-spacing-xl: 2rem;
|
||||
}
|
||||
28
packages/webui/src/types/chat.ts
Normal file
28
packages/webui/src/types/chat.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Chat message role types
|
||||
*/
|
||||
export type MessageRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
/**
|
||||
* Basic chat message structure
|
||||
*/
|
||||
export interface ChatMessage {
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan entry for task tracking
|
||||
*/
|
||||
export interface PlanEntry {
|
||||
content: string;
|
||||
priority?: 'high' | 'medium' | 'low';
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
}
|
||||
48
packages/webui/src/types/toolCall.ts
Normal file
48
packages/webui/src/types/toolCall.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tool call status
|
||||
*/
|
||||
export type ToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
|
||||
/**
|
||||
* Tool call location reference
|
||||
*/
|
||||
export interface ToolCallLocation {
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call content item
|
||||
*/
|
||||
export interface ToolCallContentItem {
|
||||
type: 'content' | 'diff';
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call update data
|
||||
*/
|
||||
export interface ToolCallUpdate {
|
||||
toolCallId: string;
|
||||
kind?: string;
|
||||
title?: string;
|
||||
status?: ToolCallStatus;
|
||||
rawInput?: unknown;
|
||||
content?: ToolCallContentItem[];
|
||||
locations?: ToolCallLocation[];
|
||||
timestamp?: number;
|
||||
}
|
||||
11
packages/webui/tailwind.config.cjs
Normal file
11
packages/webui/tailwind.config.cjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
presets: [require('./tailwind.preset.cjs')],
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
};
|
||||
71
packages/webui/tailwind.preset.cjs
Normal file
71
packages/webui/tailwind.preset.cjs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @qwen-code/webui Tailwind CSS Preset
|
||||
*
|
||||
* This preset provides shared theme configuration for all Qwen Code products.
|
||||
* Consumers should include this preset in their tailwind.config.js:
|
||||
*
|
||||
* @example
|
||||
* module.exports = {
|
||||
* presets: [require('@qwen-code/webui/tailwind.preset')],
|
||||
* content: [
|
||||
* './src/**\/*.{ts,tsx}',
|
||||
* './node_modules/@qwen-code/webui/dist/**\/*.js'
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Primary colors using CSS variables for runtime theming
|
||||
'app-primary': 'var(--app-primary, #3b82f6)',
|
||||
'app-primary-hover': 'var(--app-primary-hover, #2563eb)',
|
||||
'app-primary-foreground': 'var(--app-primary-foreground, #ffffff)',
|
||||
|
||||
// Background colors
|
||||
'app-background': 'var(--app-background, #ffffff)',
|
||||
'app-background-secondary': 'var(--app-background-secondary, #f3f4f6)',
|
||||
'app-background-tertiary': 'var(--app-background-tertiary, #e5e7eb)',
|
||||
|
||||
// Foreground/text colors
|
||||
'app-foreground': 'var(--app-foreground, #111827)',
|
||||
'app-foreground-secondary': 'var(--app-foreground-secondary, #6b7280)',
|
||||
'app-foreground-muted': 'var(--app-foreground-muted, #9ca3af)',
|
||||
|
||||
// Border colors
|
||||
'app-border': 'var(--app-border, #e5e7eb)',
|
||||
'app-border-focus': 'var(--app-border-focus, #3b82f6)',
|
||||
|
||||
// Status colors
|
||||
'app-success': 'var(--app-success, #10b981)',
|
||||
'app-warning': 'var(--app-warning, #f59e0b)',
|
||||
'app-error': 'var(--app-error, #ef4444)',
|
||||
'app-info': 'var(--app-info, #3b82f6)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['var(--app-font-sans, system-ui, sans-serif)'],
|
||||
mono: ['var(--app-font-mono, ui-monospace, monospace)'],
|
||||
},
|
||||
borderRadius: {
|
||||
'app-sm': 'var(--app-radius-sm, 0.25rem)',
|
||||
'app-md': 'var(--app-radius-md, 0.375rem)',
|
||||
'app-lg': 'var(--app-radius-lg, 0.5rem)',
|
||||
},
|
||||
spacing: {
|
||||
'app-xs': 'var(--app-spacing-xs, 0.25rem)',
|
||||
'app-sm': 'var(--app-spacing-sm, 0.5rem)',
|
||||
'app-md': 'var(--app-spacing-md, 1rem)',
|
||||
'app-lg': 'var(--app-spacing-lg, 1.5rem)',
|
||||
'app-xl': 'var(--app-spacing-xl, 2rem)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -14,11 +14,8 @@
|
|||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist",
|
||||
"emitDeclarationOnly": true
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "**/*.stories.tsx"]
|
||||
}
|
||||
|
|
|
|||
53
packages/webui/vite.config.ts
Normal file
53
packages/webui/vite.config.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'path';
|
||||
|
||||
/**
|
||||
* Vite configuration for @qwen-code/webui library
|
||||
*
|
||||
* Build outputs:
|
||||
* - ESM: dist/index.js (primary format)
|
||||
* - CJS: dist/index.cjs (compatibility)
|
||||
* - TypeScript declarations: dist/index.d.ts
|
||||
* - CSS: dist/styles.css (optional styles)
|
||||
*/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: true,
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'QwenCodeWebUI',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'react/jsx-runtime': 'jsxRuntime',
|
||||
},
|
||||
assetFileNames: 'styles.[ext]',
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
cssCodeSplit: false,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue