feat(webui): migrate icons, Tooltip, WaitingMessage from vscode-ide-companion

- Move icon components (FileIcons, EditIcons, NavigationIcons, StatusIcons,
  SpecialIcons, StopIcon) from vscode-ide-companion to webui package
- Migrate Tooltip component with CSS variable theming support
- Migrate WaitingMessage and InterruptedMessage components
- Enhance Button component with forwardRef, new variants (ghost, outline),
  loading state, and icon support
- Enhance Input component with forwardRef, error state, label, and helper text
- Update vscode-ide-companion to import components from @qwen-code/webui
- Remove replaced local components from vscode-ide-companion
- Add skipLibCheck to vscode-ide-companion tsconfig for type compatibility
This commit is contained in:
yiliang114 2026-01-15 19:53:19 +08:00
parent af76450dee
commit 71570540cc
45 changed files with 1049 additions and 308 deletions

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useState, useEffect } from 'react';

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface CloseIconProps {
@ -11,20 +17,20 @@ const CloseIcon: React.FC<CloseIconProps> = ({
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>
);
<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,405 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Edit mode related icons
*/
import type React from 'react';
import type { IconProps } from './types.js';
/**
* Edit pencil icon (16x16)
* Used for "Ask before edits" mode
*/
export const EditPencilIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Auto/fast-forward icon (16x16)
* Used for "Edit automatically" mode
*/
export const AutoEditIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M2.53 3.956A1 1 0 0 0 1 4.804v6.392a1 1 0 0 0 1.53.848l5.113-3.196c.16-.1.279-.233.357-.383v2.73a1 1 0 0 0 1.53.849l5.113-3.196a1 1 0 0 0 0-1.696L9.53 3.956A1 1 0 0 0 8 4.804v2.731a.992.992 0 0 0-.357-.383L2.53 3.956Z" />
</svg>
);
/**
* Plan mode/bars icon (16x16)
* Used for "Plan mode"
*/
export const PlanModeIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M4.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1ZM10.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1Z" />
</svg>
);
/**
* Code brackets icon (20x20)
* Used for active file indicator
*/
export const CodeBracketsIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06ZM11.377 2.011a.75.75 0 0 1 .612.867l-2.5 14.5a.75.75 0 0 1-1.478-.255l2.5-14.5a.75.75 0 0 1 .866-.612Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Hide context (eye slash) icon (20x20)
* Used to indicate the active selection will NOT be auto-loaded into context
*/
export const HideContextIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l14.5 14.5a.75.75 0 1 0 1.06-1.06l-1.745-1.745a10.029 10.029 0 0 0 3.3-4.38 1.651 1.651 0 0 0 0-1.185A10.004 10.004 0 0 0 9.999 3a9.956 9.956 0 0 0-4.744 1.194L3.28 2.22ZM7.752 6.69l1.092 1.092a2.5 2.5 0 0 1 3.374 3.373l1.091 1.092a4 4 0 0 0-5.557-5.557Z"
clipRule="evenodd"
/>
<path d="m10.748 13.93 2.523 2.523a9.987 9.987 0 0 1-3.27.547c-4.258 0-7.894-2.66-9.337-6.41a1.651 1.651 0 0 1 0-1.186A10.007 10.007 0 0 1 2.839 6.02L6.07 9.252a4 4 0 0 0 4.678 4.678Z" />
</svg>
);
/**
* Slash command icon (20x20)
* Used for command menu button
*/
export const SlashCommandIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M12.528 3.047a.75.75 0 0 1 .449.961L8.433 16.504a.75.75 0 1 1-1.41-.512l4.544-12.496a.75.75 0 0 1 .961-.449Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Link/attachment icon (20x20)
* Used for attach context button
*/
export const LinkIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M15.621 4.379a3 3 0 0 0-4.242 0l-7 7a3 3 0 0 0 4.241 4.243h.001l.497-.5a.75.75 0 0 1 1.064 1.057l-.498.501-.002.002a4.5 4.5 0 0 1-6.364-6.364l7-7a4.5 4.5 0 0 1 6.368 6.36l-3.455 3.553A2.625 2.625 0 1 1 9.52 9.52l3.45-3.451a.75.75 0 1 1 1.061 1.06l-3.45 3.451a1.125 1.125 0 0 0 1.587 1.595l3.454-3.553a3 3 0 0 0 0-4.242Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Open diff icon (16x16)
* Used for opening diff in VS Code
*/
export const OpenDiffIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M13.5 7l-4-4v3h-6v2h6v3l4-4z" />
</svg>
);
/**
* Undo edit icon (16x16)
* Used for undoing edits in diff views
*/
export const UndoIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
d="M9 10.6667L12.3333 14L9 17.3333M12.3333 14H4.66667C3.56112 14 2.66667 13.1056 2.66667 12V4.66667C2.66667 3.56112 3.56112 2.66667 4.66667 2.66667H13.3333C14.4389 2.66667 15.3333 3.56112 15.3333 4.66667V8.66667"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* Redo edit icon (16x16)
* Used for redoing edits in diff views
*/
export const RedoIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
d="M7 10.6667L3.66667 14L7 17.3333M3.66667 14H11.3333C12.4389 14 13.3333 13.1056 13.3333 12V4.66667C13.3333 3.56112 12.4389 2.66667 11.3333 2.66667H2.66667C1.56112 2.66667 0.666667 3.56112 0.666667 4.66667V8.66667"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* Replace all icon (16x16)
* Used for replacing all occurrences in search/replace
*/
export const ReplaceAllIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
d="M11.3333 5.33333L14 8L11.3333 10.6667M14 8H6C3.79086 8 2 9.79086 2 12M2.66667 10.6667L0 8L2.66667 5.33333M2.66667 8H10C12.2091 8 14 6.20914 14 4V4"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* Copy icon (16x16)
* Used for copying content
*/
export const CopyIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<rect
x="4.6665"
y="4"
width="8"
height="8"
rx="1.33333"
stroke="currentColor"
strokeWidth="1.33333"
/>
<path
d="M6 6H5.33333C4.04767 6 3 7.04767 3 8.33333V10.6667C3 11.9523 4.04767 13 5.33333 13H7.66667C8.95233 13 10 11.9523 10 10.6667V10"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
/>
</svg>
);
/**
* Paste icon (16x16)
* Used for pasting content
*/
export const PasteIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
d="M5.3335 4.66669V4.00002C5.3335 3.62305 5.48315 3.26159 5.75181 2.99293C6.02047 2.72427 6.38193 2.57467 6.7589 2.57467H9.2589C9.63587 2.57467 9.99733 2.72427 10.266 2.99293C10.5346 3.26159 10.6842 3.62305 10.6842 4.00002V4.66669M12.0176 4.66669H12.6842C13.0612 4.66669 13.4227 4.81628 13.6913 5.08494C13.96 5.3536 14.1096 5.71506 14.1096 6.09203V10.9254C14.1096 11.3023 13.96 11.6638 13.6913 11.9325C13.4227 12.2011 13.0612 12.3507 12.6842 12.3507H3.35089C2.97392 12.3507 2.61246 12.2011 2.3438 11.9325C2.07514 11.6638 1.92554 11.3023 1.92554 10.9254V6.09203C1.92554 5.71506 2.07514 5.3536 2.3438 5.08494C2.61246 4.81628 2.97392 4.66669 3.35089 4.66669H4.01756M12.0176 4.66669V7.33335C12.0176 8.06973 11.7253 8.77607 11.2093 9.29205C10.6933 9.80803 9.98698 10.0999 9.2506 10.0999H6.77573C6.03935 10.0999 5.33301 9.80803 4.81703 9.29205C4.30105 8.77607 4.00918 8.06973 4.00918 7.33335V4.66669M12.0176 4.66669H4.01756"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* Select all icon (16x16)
* Used for selecting all content
*/
export const SelectAllIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<rect
x="2.6665"
y="2"
width="10.6667"
height="12"
rx="1.33333"
stroke="currentColor"
strokeWidth="1.33333"
/>
<path
d="M5.3335 5.33333H8.00016M5.3335 8H10.6668M5.3335 10.6667H10.6668"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
/>
</svg>
);

View file

@ -0,0 +1,103 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* File and document related icons
*/
import type React from 'react';
import type { IconProps } from './types.js';
/**
* File document icon (16x16)
* Used for file completion menu
*/
export const FileIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M9 2H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7l-5-5zm3 7V3.5L10.5 2H10v3a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V2H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1zM6 3h3v2H6V3z" />
</svg>
);
export const FileListIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M5 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Z" />
</svg>
);
/**
* Save document icon (16x16)
* Used for save session button
*/
export const SaveDocumentIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M2.66663 2.66663H10.6666L13.3333 5.33329V13.3333H2.66663V2.66663Z" />
<path d="M8 10.6666V8M8 8V5.33329M8 8H10.6666M8 8H5.33329" />
</svg>
);
/**
* Folder icon (16x16)
* Useful for directory entries in completion lists
*/
export const FolderIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M1.5 3A1.5 1.5 0 0 1 3 1.5h3.086a1.5 1.5 0 0 1 1.06.44L8.5 3H13A1.5 1.5 0 0 1 14.5 4.5v7A1.5 1.5 0 0 1 13 13H3A1.5 1.5 0 0 1 1.5 11.5v-8Z" />
</svg>
);

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface IconProps {
@ -12,27 +18,24 @@ const Icon: React.FC<IconProps> = ({
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}
<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"
>
<text
x="50%"
y="50%"
dominantBaseline="middle"
textAnchor="middle"
fontSize="10"
>
{name}
</text>
</svg>
)
;
{name}
</text>
</svg>
);
export default Icon;

View file

@ -0,0 +1,212 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Navigation and action icons
*/
import type React from 'react';
import type { IconProps } from './types.js';
/**
* Chevron down icon (20x20)
* Used for dropdown arrows
*/
export const ChevronDownIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Plus icon (20x20)
* Used for new session button
*/
export const PlusIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
</svg>
);
/**
* Small plus icon (16x16)
* Used for default attachment type
*/
export const PlusSmallIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M8 2a.5.5 0 0 1 .5.5V5h2.5a.5.5 0 0 1 0 1H8.5v2.5a.5.5 0 0 1-1 0V6H5a.5.5 0 0 1 0-1h2.5V2.5A.5.5 0 0 1 8 2Z" />
</svg>
);
/**
* Arrow up icon (20x20)
* Used for send message button
*/
export const ArrowUpIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Close X icon (14x14)
* Used for close buttons in banners and dialogs
*/
export const CloseIcon: React.FC<IconProps> = ({
size = 14,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
d="M1 1L13 13M1 13L13 1"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
export const CloseSmallIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708Z" />
</svg>
);
/**
* Search/magnifying glass icon (20x20)
* Used for search input
*/
export const SearchIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z"
clipRule="evenodd"
/>
</svg>
);
/**
* Refresh/reload icon (16x16)
* Used for refresh session list
*/
export const RefreshIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M13.3333 8C13.3333 10.9455 10.9455 13.3333 8 13.3333C5.05451 13.3333 2.66663 10.9455 2.66663 8C2.66663 5.05451 5.05451 2.66663 8 2.66663" />
<path d="M10.6666 8L13.3333 8M13.3333 8L13.3333 5.33333M13.3333 8L10.6666 10.6667" />
</svg>
);

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface SendIconProps {
@ -11,20 +17,20 @@ const SendIcon: React.FC<SendIconProps> = ({
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>
);
<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,79 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Special UI icons
*/
import type React from 'react';
import type { IconProps } from './types.js';
interface ThinkingIconProps extends IconProps {
/**
* Whether thinking is enabled (affects styling)
*/
enabled?: boolean;
}
export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
size = 16,
className,
enabled = false,
style,
...props
}) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden="true"
{...props}
>
<path
d="M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915 1.30834 14.8848 4.31624 14.8848 8C14.8848 11.8025 11.8025 14.8848 8 14.8848C4.19752 14.8848 1.11523 11.8025 1.11523 8C1.11523 7.67691 1.37711 7.41504 1.7002 7.41504C2.02319 7.41514 2.28516 7.67698 2.28516 8C2.28516 11.1563 4.84369 13.7148 8 13.7148C11.1563 13.7148 13.7148 11.1563 13.7148 8C13.7148 4.94263 11.3141 2.4464 8.29492 2.29297V2.29199L7.99609 2.28516H7.9873V2.28418L7.89648 2.27539L7.88281 2.27441V2.27344C7.61596 2.21897 7.41513 1.98293 7.41504 1.7002C7.41504 1.37711 7.67691 1.11523 8 1.11523H8.00293ZM8 3.81543C8.32309 3.81543 8.58496 4.0773 8.58496 4.40039V7.6377L10.9619 8.82715C11.2505 8.97169 11.3678 9.32256 11.2236 9.61133C11.0972 9.86425 10.8117 9.98544 10.5488 9.91504L10.5352 9.91211V9.91016L10.4502 9.87891L10.4385 9.87402V9.87305L7.73828 8.52344C7.54007 8.42433 7.41504 8.22155 7.41504 8V4.40039C7.41504 4.0773 7.67691 3.81543 8 3.81543ZM2.44336 5.12695C2.77573 5.19517 3.02597 5.48929 3.02637 5.8418C3.02637 6.19456 2.7761 6.49022 2.44336 6.55859L2.2959 6.57324C1.89241 6.57324 1.56543 6.24529 1.56543 5.8418C1.56588 5.43853 1.89284 5.1123 2.2959 5.1123L2.44336 5.12695ZM3.46094 2.72949C3.86418 2.72984 4.19017 3.05712 4.19043 3.45996V3.46094C4.19009 3.86393 3.86392 4.19008 3.46094 4.19043H3.45996C3.05712 4.19017 2.72983 3.86419 2.72949 3.46094V3.45996C2.72976 3.05686 3.05686 2.72976 3.45996 2.72949H3.46094ZM5.98926 1.58008C6.32235 1.64818 6.57324 1.94276 6.57324 2.2959L6.55859 2.44336C6.49022 2.7761 6.19456 3.02637 5.8418 3.02637C5.43884 3.02591 5.11251 2.69895 5.1123 2.2959L5.12695 2.14844C5.19504 1.81591 5.48906 1.56583 5.8418 1.56543L5.98926 1.58008Z"
strokeWidth="0.27"
style={{
stroke: enabled
? 'var(--app-qwen-ivory)'
: 'var(--app-secondary-foreground)',
fill: enabled
? 'var(--app-qwen-ivory)'
: 'var(--app-secondary-foreground)',
...style,
}}
/>
</svg>
);
export const TerminalIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M5.14648 7.14648C5.34175 6.95122 5.65825 6.95122 5.85352 7.14648L8.35352 9.64648C8.44728 9.74025 8.5 9.86739 8.5 10C8.5 10.0994 8.47037 10.1958 8.41602 10.2773L8.35352 10.3535L5.85352 12.8535C5.65825 13.0488 5.34175 13.0488 5.14648 12.8535C4.95122 12.6583 4.95122 12.3417 5.14648 12.1465L7.29297 10L5.14648 7.85352C4.95122 7.65825 4.95122 7.34175 5.14648 7.14648Z"
clipRule="evenodd"
/>
<path d="M14.5 12C14.7761 12 15 12.2239 15 12.5C15 12.7761 14.7761 13 14.5 13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H14.5Z" />
<path
fillRule="evenodd"
d="M16.5 4C17.3284 4 18 4.67157 18 5.5V14.5C18 15.3284 17.3284 16 16.5 16H3.5C2.67157 16 2 15.3284 2 14.5V5.5C2 4.67157 2.67157 4 3.5 4H16.5ZM3.5 5C3.22386 5 3 5.22386 3 5.5V14.5C3 14.7761 3.22386 15 3.5 15H16.5C16.7761 15 17 14.7761 17 14.5V5.5C17 5.22386 16.7761 5 16.5 5H3.5Z"
clipRule="evenodd"
/>
</svg>
);

View file

@ -0,0 +1,188 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Status and state related icons
*/
import type React from 'react';
import type { IconProps } from './types.js';
/**
* Plan completed icon (14x14)
* Used for completed plan items
*/
export const PlanCompletedIcon: React.FC<IconProps> = ({
size = 14,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<circle cx="7" cy="7" r="6" fill="currentColor" opacity="0.2" />
<path
d="M4 7.5L6 9.5L10 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* Plan in progress icon (14x14)
* Used for in-progress plan items
*/
export const PlanInProgressIcon: React.FC<IconProps> = ({
size = 14,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<circle
cx="7"
cy="7"
r="5"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
/>
</svg>
);
/**
* Plan pending icon (14x14)
* Used for pending plan items
*/
export const PlanPendingIcon: React.FC<IconProps> = ({
size = 14,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<circle
cx="7"
cy="7"
r="5.5"
fill="none"
stroke="currentColor"
strokeWidth="1"
/>
</svg>
);
/**
* Warning triangle icon (20x20)
* Used for warning messages
*/
export const WarningTriangleIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path
fillRule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clipRule="evenodd"
/>
</svg>
);
/**
* User profile icon (16x16)
* Used for login command
*/
export const UserIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" />
</svg>
);
export const SymbolIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M8 1a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 7.293V1.5A.5.5 0 0 1 8 1Z" />
</svg>
);
export const SelectionIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<path d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Z" />
</svg>
);

View file

@ -0,0 +1,33 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Stop icon for canceling operations
*/
import type React from 'react';
import type { IconProps } from './types.js';
/**
* Stop/square icon (16x16)
* Used for stop/cancel operations
*/
export const StopIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width={size}
height={size}
className={className}
aria-hidden="true"
{...props}
>
<rect x="4" y="4" width="8" height="8" rx="1" />
</svg>
);

View file

@ -0,0 +1,57 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export type { IconProps } from './types.js';
// File icons
export {
FileIcon,
FileListIcon,
SaveDocumentIcon,
FolderIcon,
} from './FileIcons.js';
// Navigation icons
export {
ChevronDownIcon,
PlusIcon,
PlusSmallIcon,
ArrowUpIcon,
CloseIcon,
CloseSmallIcon,
SearchIcon,
RefreshIcon,
} from './NavigationIcons.js';
// Edit mode icons
export {
EditPencilIcon,
AutoEditIcon,
PlanModeIcon,
CodeBracketsIcon,
HideContextIcon,
SlashCommandIcon,
LinkIcon,
OpenDiffIcon,
UndoIcon,
} from './EditIcons.js';
// Status icons
export {
PlanCompletedIcon,
PlanInProgressIcon,
PlanPendingIcon,
WarningTriangleIcon,
UserIcon,
SymbolIcon,
SelectionIcon,
} from './StatusIcons.js';
// Special icons
export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js';
// Stop icon
export { StopIcon } from './StopIcon.js';

View file

@ -0,0 +1,22 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Common icon props interface
*/
import type React from 'react';
export interface IconProps extends React.SVGProps<SVGSVGElement> {
/**
* Icon size (width and height)
* @default 16
*/
size?: number;
/**
* Additional CSS classes
*/
className?: string;
}

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface ContainerProps {
@ -6,7 +12,7 @@ interface ContainerProps {
}
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => (
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
);
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
);
export default Container;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
const Footer: React.FC = () => <footer>Footer Component Placeholder</footer>;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
const Header: React.FC = () => <header>Header Component Placeholder</header>;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
const Main: React.FC = () => <main>Main Component Placeholder</main>;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
const Sidebar: React.FC = () => <aside>Sidebar Component Placeholder</aside>;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface MessageProps {

View file

@ -1,5 +1,13 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
const MessageInput: React.FC = () => <div>MessageInput Component Placeholder</div>;
const MessageInput: React.FC = () => (
<div>MessageInput Component Placeholder</div>
);
export default MessageInput;

View file

@ -1,5 +1,13 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
const MessageList: React.FC = () => <div>MessageList Component Placeholder</div>;
const MessageList: React.FC = () => (
<div>MessageList Component Placeholder</div>
);
export default MessageList;

View file

@ -0,0 +1,22 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface InterruptedMessageProps {
text?: string;
}
// A lightweight status line similar to WaitingMessage but without the left status icon.
export const InterruptedMessage: React.FC<InterruptedMessageProps> = ({
text = 'Interrupted',
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85">
<div className="interrupted-item w-full relative">
<span className="opacity-70 italic">{text}</span>
</div>
</div>
);

View file

@ -0,0 +1,85 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
interface WaitingMessageProps {
loadingMessage: string;
}
// Rotate message every few seconds while waiting
const ROTATE_INTERVAL_MS = 3000; // rotate every 3s per request
// Default witty loading phrases
const DEFAULT_LOADING_PHRASES = [
'Processing...',
'Working on it...',
'Just a moment...',
'Loading...',
'Hold tight...',
'Almost there...',
];
export const WaitingMessage: React.FC<WaitingMessageProps> = ({
loadingMessage,
}) => {
// Build a phrase list that starts with the provided message (if any), then witty fallbacks
const phrases = useMemo(() => {
const set = new Set<string>();
const list: string[] = [];
if (loadingMessage && loadingMessage.trim()) {
list.push(loadingMessage);
set.add(loadingMessage);
}
for (const p of DEFAULT_LOADING_PHRASES) {
if (!set.has(p)) {
list.push(p);
}
}
return list;
}, [loadingMessage]);
const [index, setIndex] = useState(0);
// Reset to the first phrase whenever the incoming message changes
useEffect(() => {
setIndex(0);
}, [phrases]);
// Periodically rotate to a different phrase
useEffect(() => {
if (phrases.length <= 1) {
return;
}
const id = setInterval(() => {
setIndex((prev) => {
// pick a different random index to avoid immediate repeats
let next = Math.floor(Math.random() * phrases.length);
if (phrases.length > 1) {
let guard = 0;
while (next === prev && guard < 5) {
next = Math.floor(Math.random() * phrases.length);
guard++;
}
}
return next;
});
}, ROTATE_INTERVAL_MS);
return () => clearInterval(id);
}, [phrases]);
return (
<div className="waiting-message-outer flex gap-0 items-start text-left py-2 flex-col opacity-85">
{/* Use the same left status icon (pseudo-element) style as assistant-message-container */}
<div className="assistant-message-container assistant-message-loading waiting-message-inner w-full items-start pl-[30px] relative">
<span className="waiting-message-text opacity-70 italic loading-text-shimmer">
{phrases[index]}
</span>
</div>
</div>
);
};

View file

@ -1,49 +1,143 @@
import type React from 'react';
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
interface ButtonProps {
import type React from 'react';
import { forwardRef } from 'react';
/**
* Button variant types
*/
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'danger'
| 'ghost'
| 'outline';
/**
* Button size types
*/
export type ButtonSize = 'sm' | 'md' | 'lg';
/**
* Button component props interface
*/
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Button content */
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
className?: string;
/** Visual style variant */
variant?: ButtonVariant;
/** Button size */
size?: ButtonSize;
/** Loading state - shows spinner and disables button */
loading?: boolean;
/** Icon to display before children */
leftIcon?: React.ReactNode;
/** Icon to display after children */
rightIcon?: React.ReactNode;
/** Full width button */
fullWidth?: boolean;
}
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';
/**
* Button component with multiple variants and sizes
*
* @example
* ```tsx
* <Button variant="primary" size="md" onClick={handleClick}>
* Click me
* </Button>
* ```
*/
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
leftIcon,
rightIcon,
fullWidth = false,
className = '',
type = 'button',
...props
},
ref,
) => {
const isDisabled = disabled || loading;
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 baseClasses =
'inline-flex items-center justify-center rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
const sizeClasses = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
const variantClasses: Record<ButtonVariant, string> = {
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',
ghost:
'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-400',
outline:
'bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-400',
};
const disabledClass = disabled ? 'opacity-50 cursor-not-allowed' : '';
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-2 py-1 text-sm gap-1',
md: 'px-4 py-2 gap-2',
lg: 'px-6 py-3 text-lg gap-2',
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClass} ${className}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
const disabledClass = isDisabled
? 'opacity-50 cursor-not-allowed pointer-events-none'
: '';
const widthClass = fullWidth ? 'w-full' : '';
return (
<button
ref={ref}
type={type}
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClass} ${widthClass} ${className}`.trim()}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-busy={loading}
{...props}
>
{loading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
)}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
</button>
);
},
);
Button.displayName = 'Button';
export default Button;

View file

@ -1,25 +1,149 @@
import type React from 'react';
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
interface InputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
import type React from 'react';
import { forwardRef } from 'react';
/**
* Input size types
*/
export type InputSize = 'sm' | 'md' | 'lg';
/**
* Input component props interface
*/
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
/** Input size */
size?: InputSize;
/** Error state */
error?: boolean;
/** Error message to display */
errorMessage?: string;
/** Label for the input */
label?: string;
/** Helper text below input */
helperText?: string;
/** Left icon/element */
leftElement?: React.ReactNode;
/** Right icon/element */
rightElement?: React.ReactNode;
/** Full width input */
fullWidth?: boolean;
}
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}`}
/>
);
/**
* Input component with multiple sizes and states
*
* @example
* ```tsx
* <Input
* label="Email"
* placeholder="Enter your email"
* error={!!errors.email}
* errorMessage={errors.email}
* />
* ```
*/
const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
size = 'md',
error = false,
errorMessage,
label,
helperText,
leftElement,
rightElement,
fullWidth = false,
className = '',
id,
disabled,
...props
},
ref,
) => {
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
const baseClasses =
'border rounded transition-colors focus:outline-none focus:ring-2';
const sizeClasses: Record<InputSize, string> = {
sm: 'px-2 py-1 text-sm',
md: 'px-3 py-2',
lg: 'px-4 py-3 text-lg',
};
const stateClasses = error
? 'border-red-500 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500';
const disabledClasses = disabled
? 'bg-gray-100 cursor-not-allowed opacity-60'
: 'bg-white';
const widthClass = fullWidth ? 'w-full' : '';
const paddingClasses = [
leftElement ? 'pl-10' : '',
rightElement ? 'pr-10' : '',
].join(' ');
return (
<div className={`${fullWidth ? 'w-full' : 'inline-block'}`}>
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-700 mb-1"
>
{label}
</label>
)}
<div className="relative">
{leftElement && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
{leftElement}
</div>
)}
<input
ref={ref}
id={inputId}
disabled={disabled}
aria-invalid={error}
aria-describedby={
errorMessage
? `${inputId}-error`
: helperText
? `${inputId}-helper`
: undefined
}
className={`${baseClasses} ${sizeClasses[size]} ${stateClasses} ${disabledClasses} ${widthClass} ${paddingClasses} ${className}`.trim()}
{...props}
/>
{rightElement && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500">
{rightElement}
</div>
)}
</div>
{errorMessage && error && (
<p id={`${inputId}-error`} className="mt-1 text-sm text-red-600">
{errorMessage}
</p>
)}
{helperText && !error && (
<p id={`${inputId}-helper`} className="mt-1 text-sm text-gray-500">
{helperText}
</p>
)}
</div>
);
},
);
Input.displayName = 'Input';
export default Input;

View file

@ -1,93 +1,73 @@
import React, { useState } from 'react';
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
interface ChildProps {
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onFocus?: () => void;
onBlur?: () => void;
tabIndex?: number;
import type React from 'react';
/**
* Tooltip component props
*/
export interface TooltipProps {
/** Content to wrap with tooltip */
children: React.ReactNode;
/** Tooltip content (can be string or ReactNode) */
content: React.ReactNode;
/** Tooltip position relative to children */
position?: 'top' | 'bottom' | 'left' | 'right';
}
interface TooltipProps {
children: React.ReactElement<ChildProps>;
content: string;
position?: 'top' | 'right' | 'bottom' | 'left';
}
const Tooltip: React.FC<TooltipProps> = ({
/**
* Tooltip component using CSS group-hover for display
* Supports CSS variables for theming
*/
export 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 className="relative inline-block">
<div className="group relative">
{children}
<div
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
tabIndex={0}
className={`
absolute z-50 px-2 py-1 text-xs rounded-md shadow-lg
bg-[var(--app-primary-background,#1f2937)] border border-[var(--app-input-border,#374151)]
text-[var(--app-primary-foreground,#f9fafb)] whitespace-nowrap
opacity-0 group-hover:opacity-100 transition-opacity duration-150
-translate-x-1/2 left-1/2
${
position === 'top'
? '-translate-y-1 bottom-full mb-1'
: position === 'bottom'
? 'translate-y-1 top-full mt-1'
: position === 'left'
? '-translate-x-full left-0 translate-y-[-50%] top-1/2'
: 'translate-x-0 right-0 translate-y-[-50%] top-1/2'
}
pointer-events-none
`}
>
{React.cloneElement(children, {
onMouseEnter: () => {
setIsVisible(true);
const typedChildren = children as React.ReactElement<ChildProps>;
if (typeof typedChildren.props.onMouseEnter === 'function') {
typedChildren.props.onMouseEnter();
{content}
<div
className={`
absolute w-2 h-2 bg-[var(--app-primary-background,#1f2937)] border-l border-b border-[var(--app-input-border,#374151)]
-rotate-45
${
position === 'top'
? 'top-full left-1/2 -translate-x-1/2 -translate-y-1/2'
: position === 'bottom'
? 'bottom-full left-1/2 -translate-x-1/2 translate-y-1/2'
: position === 'left'
? 'right-full top-1/2 translate-x-1/2 -translate-y-1/2'
: 'left-full top-1/2 -translate-x-1/2 -translate-y-1/2'
}
},
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>
);
};
</div>
);
export default Tooltip;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { useState } from 'react';
export const useLocalStorage = <T>(key: string, initialValue: T) => {

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect } from 'react';
export const useTheme = () => {

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
// Shared UI Components Export
// Export all shared components from this package
@ -24,11 +30,14 @@ export { default as Footer } from './components/layout/Footer';
export { default as Message } from './components/messages/Message';
export { default as MessageInput } from './components/messages/MessageInput';
export { default as MessageList } from './components/messages/MessageList';
export { WaitingMessage } from './components/messages/Waiting/WaitingMessage';
export { InterruptedMessage } from './components/messages/Waiting/InterruptedMessage';
// 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';
export { Tooltip } from './components/ui/Tooltip';
export type { TooltipProps } from './components/ui/Tooltip';
// Permission components
export { default as PermissionDrawer } from './components/PermissionDrawer';
@ -38,6 +47,56 @@ export { default as Icon } from './components/icons/Icon';
export { default as CloseIcon } from './components/icons/CloseIcon';
export { default as SendIcon } from './components/icons/SendIcon';
// File Icons
export {
FileIcon,
FileListIcon,
SaveDocumentIcon,
FolderIcon,
} from './components/icons/FileIcons';
// Status Icons
export {
PlanCompletedIcon,
PlanInProgressIcon,
PlanPendingIcon,
WarningTriangleIcon,
UserIcon,
SymbolIcon,
SelectionIcon,
} from './components/icons/StatusIcons';
// Navigation Icons
export {
ChevronDownIcon,
PlusIcon,
PlusSmallIcon,
ArrowUpIcon,
CloseIcon as CloseXIcon,
CloseSmallIcon,
SearchIcon,
RefreshIcon,
} from './components/icons/NavigationIcons';
// Edit Icons
export {
EditPencilIcon,
AutoEditIcon,
PlanModeIcon,
CodeBracketsIcon,
HideContextIcon,
SlashCommandIcon,
LinkIcon,
OpenDiffIcon,
UndoIcon,
} from './components/icons/EditIcons';
// Special Icons
export { ThinkingIcon, TerminalIcon } from './components/icons/SpecialIcons';
// Action Icons
export { StopIcon } from './components/icons/StopIcon';
// Hooks
export { useTheme } from './hooks/useTheme';
export { useLocalStorage } from './hooks/useLocalStorage';

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export interface MessageProps {
id: string;
content: string;

View file

@ -1 +1,7 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export type Theme = 'light' | 'dark' | 'auto';