chore(webui): rename

This commit is contained in:
yiliang114 2026-01-14 23:27:56 +08:00
parent 1e2ef871d7
commit ec0586b135
25 changed files with 724 additions and 0 deletions

View file

@ -152,6 +152,7 @@
"vitest": "^3.2.4"
},
"dependencies": {
"@qwen-code/webui": "workspace:*",
"semver": "^7.7.2",
"@modelcontextprotocol/sdk": "^1.25.1",
"cors": "^2.8.5",

View file

@ -0,0 +1,78 @@
// Example of how to use shared UI components
// This would typically be integrated into existing components
import React, { useState } from 'react';
import {
Button,
Input,
Message,
PermissionDrawer,
Tooltip,
} from '@qwen-code/webui';
const ExampleComponent: React.FC = () => {
const [inputValue, setInputValue] = useState('');
const [showPermissionDrawer, setShowPermissionDrawer] = useState(false);
const handleConfirmPermission = () => {
console.log('Permissions confirmed');
setShowPermissionDrawer(false);
};
return (
<div className="p-4">
<h2 className="text-lg font-bold mb-4">Shared Components Demo</h2>
{/* Example of using shared Button component */}
<div className="mb-4">
<Button
variant="primary"
size="md"
onClick={() => setShowPermissionDrawer(true)}
>
Show Permission Drawer
</Button>
</div>
{/* Example of using shared Input component */}
<div className="mb-4">
<Input
value={inputValue}
onChange={setInputValue}
placeholder="Type something..."
/>
</div>
{/* Example of using shared Message component */}
<div className="mb-4">
<Message
id="demo-message"
content="This is a shared message component"
sender="system"
timestamp={new Date()}
/>
</div>
{/* Example of using shared Tooltip component */}
<div className="mb-4">
<Tooltip content="This is a helpful tooltip" position="top">
<Button variant="secondary">Hover for tooltip</Button>
</Tooltip>
</div>
{/* Example of using shared PermissionDrawer component */}
<PermissionDrawer
isOpen={showPermissionDrawer}
onClose={() => setShowPermissionDrawer(false)}
onConfirm={handleConfirmPermission}
permissions={[
'Access browser history',
'Read current page',
'Capture screenshots',
]}
/>
</div>
);
};
export default ExampleComponent;

View file

@ -0,0 +1,36 @@
{
"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",
"scripts": {
"build": "tsc && rollup -c",
"dev": "tsc --watch",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"rollup": "^4.0.0",
"rollup-plugin-dts": "^6.0.0"
},
"keywords": [
"qwen",
"ui",
"components",
"shared"
],
"author": "Qwen Team",
"license": "MIT"
}

View file

@ -0,0 +1,47 @@
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()],
},
];

View file

@ -0,0 +1,93 @@
import type React from 'react';
import { useState, useEffect } from 'react';
interface PermissionDrawerProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
permissions: string[];
}
const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
isOpen,
onClose,
onConfirm,
permissions,
}) => {
const [checkedPermissions, setCheckedPermissions] = useState<boolean[]>(
Array(permissions.length).fill(false),
);
useEffect(() => {
if (!isOpen) {
setCheckedPermissions(Array(permissions.length).fill(false));
}
}, [isOpen, permissions]);
const handleTogglePermission = (index: number) => {
const newChecked = [...checkedPermissions];
newChecked[index] = !newChecked[index];
setCheckedPermissions(newChecked);
};
const handleConfirm = () => {
onConfirm();
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-96 max-h-96 overflow-y-auto">
<div className="p-4 border-b">
<h2 className="text-lg font-semibold">Permissions Required</h2>
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="p-4">
<ul className="space-y-2">
{permissions.map((permission, index) => (
<li key={index} className="flex items-center">
<input
type="checkbox"
checked={checkedPermissions[index]}
onChange={() => handleTogglePermission(index)}
className="mr-2 h-4 w-4"
/>
<span>{permission}</span>
</li>
))}
</ul>
</div>
<div className="p-4 border-t flex justify-end space-x-2">
<button
onClick={onClose}
className="px-4 py-2 border border-gray-300 rounded hover:bg-gray-100"
>
Cancel
</button>
<button
onClick={handleConfirm}
disabled={!checkedPermissions.every((p) => p)}
className={`px-4 py-2 rounded ${
checkedPermissions.every((p) => p)
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
Confirm
</button>
</div>
</div>
</div>
);
};
export default PermissionDrawer;

View file

@ -0,0 +1,30 @@
import type React from 'react';
interface CloseIconProps {
size?: number;
color?: string;
className?: string;
}
const CloseIcon: React.FC<CloseIconProps> = ({
size = 24,
color = 'currentColor',
className = '',
}) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
);
export default CloseIcon;

View file

@ -0,0 +1,38 @@
import type React from 'react';
interface IconProps {
name: string;
size?: number;
color?: string;
className?: string;
}
const Icon: React.FC<IconProps> = ({
name,
size = 24,
color = 'currentColor',
className = '',
}) =>
// This is a placeholder - in a real implementation you might use an icon library
(
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill={color}
className={className}
>
<text
x="50%"
y="50%"
dominantBaseline="middle"
textAnchor="middle"
fontSize="10"
>
{name}
</text>
</svg>
)
;
export default Icon;

View file

@ -0,0 +1,30 @@
import type React from 'react';
interface SendIconProps {
size?: number;
color?: string;
className?: string;
}
const SendIcon: React.FC<SendIconProps> = ({
size = 24,
color = 'currentColor',
className = '',
}) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
);
export default SendIcon;

View file

@ -0,0 +1,12 @@
import type React from 'react';
interface ContainerProps {
children: React.ReactNode;
className?: string;
}
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => (
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
);
export default Container;

View file

@ -0,0 +1,5 @@
import type React from 'react';
const Footer: React.FC = () => <footer>Footer Component Placeholder</footer>;
export default Footer;

View file

@ -0,0 +1,5 @@
import type React from 'react';
const Header: React.FC = () => <header>Header Component Placeholder</header>;
export default Header;

View file

@ -0,0 +1,5 @@
import type React from 'react';
const Main: React.FC = () => <main>Main Component Placeholder</main>;
export default Main;

View file

@ -0,0 +1,5 @@
import type React from 'react';
const Sidebar: React.FC = () => <aside>Sidebar Component Placeholder</aside>;
export default Sidebar;

View file

@ -0,0 +1,39 @@
import type React from 'react';
interface MessageProps {
id: string;
content: string;
sender: 'user' | 'system' | 'assistant';
timestamp?: Date;
className?: string;
}
const Message: React.FC<MessageProps> = ({
content,
sender,
timestamp,
className = '',
}) => {
const alignment = sender === 'user' ? 'justify-end' : 'justify-start';
const bgColor = sender === 'user' ? 'bg-blue-500' : 'bg-gray-200';
return (
<div className={`flex ${alignment} mb-4 ${className}`}>
<div
className={`${bgColor} text-white rounded-lg px-4 py-2 max-w-xs md:max-w-md lg:max-w-lg`}
>
{content}
{timestamp && (
<div className="text-xs opacity-70 mt-1">
{timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</div>
)}
</div>
</div>
);
};
export default Message;

View file

@ -0,0 +1,5 @@
import type React from 'react';
const MessageInput: React.FC = () => <div>MessageInput Component Placeholder</div>;
export default MessageInput;

View file

@ -0,0 +1,5 @@
import type React from 'react';
const MessageList: React.FC = () => <div>MessageList Component Placeholder</div>;
export default MessageList;

View file

@ -0,0 +1,49 @@
import type React from 'react';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
className?: string;
}
const Button: React.FC<ButtonProps> = ({
children,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
className = '',
}) => {
const baseClasses =
'inline-flex items-center justify-center rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary:
'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
};
const sizeClasses = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
const disabledClass = disabled ? 'opacity-50 cursor-not-allowed' : '';
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClass} ${className}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export default Button;

View file

@ -0,0 +1,25 @@
import type React from 'react';
interface InputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}
const Input: React.FC<InputProps> = ({
value,
onChange,
placeholder,
className = '',
}) => (
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className={`border rounded px-3 py-2 ${className}`}
/>
);
export default Input;

View file

@ -0,0 +1,93 @@
import React, { useState } from 'react';
interface ChildProps {
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onFocus?: () => void;
onBlur?: () => void;
tabIndex?: number;
}
interface TooltipProps {
children: React.ReactElement<ChildProps>;
content: string;
position?: 'top' | 'right' | 'bottom' | 'left';
}
const Tooltip: React.FC<TooltipProps> = ({
children,
content,
position = 'top',
}) => {
const [isVisible, setIsVisible] = useState(false);
const positionClasses = {
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
right: 'top-1/2 left-full transform -translate-y-1/2 ml-2',
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
left: 'top-1/2 right-full transform -translate-y-1/2 mr-2',
};
const arrowPositionClasses = {
top: 'top-full left-1/2 transform -translate-x-1/2 -mt-1',
right: 'top-1/2 left-0 transform -translate-y-1/2 -ml-1',
bottom: 'top-0 left-1/2 transform -translate-x-1/2 -mb-1',
left: 'top-1/2 right-0 transform -translate-y-1/2 -mr-1',
};
const tooltipClass = `absolute ${positionClasses[position]} bg-gray-800 text-white text-xs rounded py-1 px-2 pointer-events-none z-10`;
const arrowClass = `absolute w-2 h-2 bg-gray-800 transform rotate-45 ${arrowPositionClasses[position]}`;
return (
<div className="relative inline-block">
<div
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
tabIndex={0}
>
{React.cloneElement(children, {
onMouseEnter: () => {
setIsVisible(true);
const typedChildren = children as React.ReactElement<ChildProps>;
if (typeof typedChildren.props.onMouseEnter === 'function') {
typedChildren.props.onMouseEnter();
}
},
onMouseLeave: () => {
setIsVisible(false);
const typedChildren = children as React.ReactElement<ChildProps>;
if (typeof typedChildren.props.onMouseLeave === 'function') {
typedChildren.props.onMouseLeave();
}
},
onFocus: () => {
setIsVisible(true);
const typedChildren = children as React.ReactElement<ChildProps>;
if (typeof typedChildren.props.onFocus === 'function') {
typedChildren.props.onFocus();
}
},
onBlur: () => {
setIsVisible(false);
const typedChildren = children as React.ReactElement<ChildProps>;
if (typeof typedChildren.props.onBlur === 'function') {
typedChildren.props.onBlur();
}
},
tabIndex:
(children as React.ReactElement<ChildProps>).props.tabIndex || 0,
})}
</div>
{isVisible && (
<div className={tooltipClass}>
{content}
<div className={arrowClass}></div>
</div>
)}
</div>
);
};
export default Tooltip;

View file

@ -0,0 +1,27 @@
import { useState } from 'react';
export const useLocalStorage = <T>(key: string, initialValue: T) => {
// Get value from localStorage or use initial value
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (_error) {
return initialValue;
}
});
// Update localStorage when state changes
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
};

View file

@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
export const useTheme = () => {
const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as
| 'light'
| 'dark'
| 'auto'
| null;
if (savedTheme) {
setTheme(savedTheme);
} else {
const prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches;
setTheme(prefersDark ? 'dark' : 'light');
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
return { theme, toggleTheme };
};

View file

@ -0,0 +1,35 @@
// Shared UI Components Export
// Export all shared components from this package
// Layout components
export { default as Container } from './components/layout/Container';
export { default as Header } from './components/layout/Header';
export { default as Sidebar } from './components/layout/Sidebar';
export { default as Main } from './components/layout/Main';
export { default as Footer } from './components/layout/Footer';
// Message components
export { default as Message } from './components/messages/Message';
export { default as MessageInput } from './components/messages/MessageInput';
export { default as MessageList } from './components/messages/MessageList';
// UI Elements
export { default as Button } from './components/ui/Button';
export { default as Input } from './components/ui/Input';
export { default as Tooltip } from './components/ui/Tooltip';
// Permission components
export { default as PermissionDrawer } from './components/PermissionDrawer';
// Icons
export { default as Icon } from './components/icons/Icon';
export { default as CloseIcon } from './components/icons/CloseIcon';
export { default as SendIcon } from './components/icons/SendIcon';
// Hooks
export { useTheme } from './hooks/useTheme';
export { useLocalStorage } from './hooks/useLocalStorage';
// Types
export type { Theme } from './types/theme';
export type { MessageProps } from './types/messages';

View file

@ -0,0 +1,7 @@
export interface MessageProps {
id: string;
content: string;
sender: 'user' | 'system' | 'assistant';
timestamp?: Date;
className?: string;
}

View file

@ -0,0 +1 @@
export type Theme = 'light' | 'dark' | 'auto';

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"declarationDir": "./dist",
"emitDeclarationOnly": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}